aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImplIT.java39
-rw-r--r--server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java5
-rw-r--r--server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java112
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/TreeRootHolder.java7
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/AverageFormula.java161
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormula.java89
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java3
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationCheck.java2
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStep.java5
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepository.java12
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImpl.java10
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java6
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java10
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java25
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java16
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java113
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java1
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaExecutionTest.java128
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaTest.java219
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaExecutionTest.java122
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaTest.java106
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStepTest.java15
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java10
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java194
-rw-r--r--server/sonar-ce-task-projectanalysis/src/testFixtures/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryRule.java5
-rw-r--r--server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java1
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java158
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java35
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java69
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java41
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDto.java17
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java71
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java4
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java2
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java26
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java2
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java8
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java31
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml6
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml9
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml14
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml6
-rw-r--r--server/sonar-db-dao/src/schema/schema-sq.ddl40
-rw-r--r--server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java2
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphsIT.java55
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleasesIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsIT.java54
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphsIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTableIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleasesIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChangesIT.java89
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTableIT.java64
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphsIT.java56
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalysesIT.java52
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleasesIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphsIT.java59
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTableIT.java2
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTableIT.java2
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicensesIT.java69
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTableIT.java72
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRenameIT.java63
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullableIT.java66
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettingsIT.java56
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssuesTest.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfilesIT.java54
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java63
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssuesTest.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfilesIT.java53
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfilesTest.java57
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java80
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSizeTest.java55
-rw-r--r--server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java68
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java2
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java29
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphs.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleases.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsTable.java55
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphs.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTable.java52
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleases.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChanges.java69
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTable.java57
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphs.java61
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalyses.java55
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java16
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleases.java32
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphs.java33
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicenses.java90
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.java50
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRename.java33
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.java53
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettings.java35
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssues.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfiles.java57
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java67
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssues.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfiles.java58
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java43
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfiles.java32
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java50
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSize.java53
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java54
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/package-info.java23
-rw-r--r--server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504Test.java40
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java12
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java14
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java5
-rw-r--r--server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java73
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java33
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java11
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java4
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java10
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java2
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java3
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java33
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java2
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java10
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowTransition.java11
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java13
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java22
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java50
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java22
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java8
-rw-r--r--server/sonar-telemetry/src/main/java/org/sonar/telemetry/metrics/TelemetryMetricsMapper.java70
-rw-r--r--server/sonar-telemetry/src/test/java/org/sonar/telemetry/metrics/TelemetryMetricsMapperTest.java96
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/TooManyRequestsException.java27
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java7
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java15
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java28
-rw-r--r--server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/JwtHttpHandlerIT.java110
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ActiveTimeoutProvider.java26
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HardcodedActiveTimeoutProvider.java34
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java54
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java2
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/HardcodedActiveTimeoutProviderTest.java40
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java4
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupSearchRequest.java2
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java2
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/GitUrlParser.java222
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingSearchStrategy.java72
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java5
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java81
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java33
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/GitUrlParserTest.java280
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java112
-rw-r--r--server/sonar-webserver-core/src/it/java/org/sonar/server/startup/RegisterMetricsIT.java5
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java8
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java30
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/RulesRegistrationContextTest.java69
-rw-r--r--server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java108
-rw-r--r--server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java12
-rw-r--r--server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java11
-rw-r--r--server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java14
-rw-r--r--server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java63
-rw-r--r--server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java81
-rw-r--r--server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java9
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java6
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java7
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/request/GroupsSearchRestRequest.java9
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java38
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java18
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java4
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForAnonymousUsers.java1
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForLoggedInUsers.java7
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/ErrorMessages.java1
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java9
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java31
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java6
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java81
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java5
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java11
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java9
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/resources/http/http-client.env.json45
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/resources/http/project-bindings.http89
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java37
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java13
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflowTest.java169
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateCaycCheckerIT.java3
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java392
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java5
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java6
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java16
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java120
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflow.java204
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java18
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/ActiveVersionEvaluator.java28
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java2
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java6
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java7
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/batch/BatchIndexTest.java72
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java20
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/ActiveVersionEvaluatorTest.java76
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java49
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java10
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java2
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/web/NoCacheFilter.java46
-rw-r--r--server/sonar-webserver/src/test/java/org/sonar/server/platform/web/NoCacheFilterTest.java59
197 files changed, 7149 insertions, 1698 deletions
diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImplIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImplIT.java
index 1c44545518d..94fb1d8766a 100644
--- a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImplIT.java
+++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImplIT.java
@@ -88,6 +88,45 @@ public class MetricRepositoryImplIT {
}
@Test
+ public void getOptionalByKey_throws_NPE_if_arg_is_null() {
+ assertThatThrownBy(() -> underTest.getOptionalByKey(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ public void getOptionalByKey_throws_ISE_if_start_has_not_been_called() {
+ assertThatThrownBy(() -> underTest.getOptionalByKey(SOME_KEY))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Metric cache has not been initialized");
+ }
+
+ @Test
+ public void getOptionalByKey_whenMetricDoesNotExist_thenReturnsOptionalEmpty() {
+ underTest.start();
+ assertThat(underTest.getOptionalByKey(SOME_KEY)).isNotPresent();
+ }
+
+ @Test
+ public void getOptionalByKey_whenMetricIsDisabled_thenReturnsOptionalEmpty() {
+ dbTester.measures().insertMetric(t -> t.setKey("complexity").setEnabled(false));
+
+ underTest.start();
+
+ assertThat(underTest.getOptionalByKey("complexity")).isNotPresent();
+ }
+
+ @Test
+ public void getOptionalByKey_find_enabled_Metrics() {
+ MetricDto ncloc = dbTester.measures().insertMetric(t -> t.setKey("ncloc").setEnabled(true));
+ MetricDto coverage = dbTester.measures().insertMetric(t -> t.setKey("coverage").setEnabled(true));
+
+ underTest.start();
+
+ assertThat(underTest.getOptionalByKey("ncloc").get().getUuid()).isEqualTo(ncloc.getUuid());
+ assertThat(underTest.getOptionalByKey("coverage").get().getUuid()).isEqualTo(coverage.getUuid());
+ }
+
+ @Test
public void getById_throws_ISE_if_start_has_not_been_called() {
assertThatThrownBy(() -> underTest.getByUuid(SOME_UUID))
.isInstanceOf(IllegalStateException.class)
diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java
index b0aed6b3f10..a3ad472bf94 100644
--- a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java
+++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java
@@ -62,7 +62,6 @@ import static org.apache.commons.lang3.RandomStringUtils.secure;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -89,8 +88,7 @@ public class LoadPeriodsStepIT extends BaseStepTest {
private final NewCodePeriodResolver newCodePeriodResolver = new NewCodePeriodResolver(dbTester.getDbClient(), analysisMetadataHolder);
private final ZonedDateTime analysisDate = ZonedDateTime.of(2019, 3, 20, 5, 30, 40, 0, ZoneId.systemDefault());
private final CeTaskMessages ceTaskMessages = mock(CeTaskMessages.class);
- private final LoadPeriodsStep underTest = new LoadPeriodsStep(analysisMetadataHolder, dao, treeRootHolder, periodsHolder, dbTester.getDbClient(), newCodePeriodResolver,
- ceTaskMessages, system2Mock);
+ private final LoadPeriodsStep underTest = new LoadPeriodsStep(analysisMetadataHolder, dao, treeRootHolder, periodsHolder, dbTester.getDbClient(), newCodePeriodResolver);
private ComponentDto project;
@@ -198,7 +196,6 @@ public class LoadPeriodsStepIT extends BaseStepTest {
underTest.execute(new TestComputationStepContext());
assertPeriod(NewCodePeriodType.REFERENCE_BRANCH, newCodeReferenceBranch, null);
- verify(ceTaskMessages).add(any(CeTaskMessages.Message.class));
}
@Test
diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java
new file mode 100644
index 00000000000..0ede18e3ccf
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java
@@ -0,0 +1,112 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.ce.task.projectanalysis.step;
+
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
+import org.sonar.ce.task.projectanalysis.analysis.TestBranch;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.ReportComponent;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
+import org.sonar.ce.task.projectanalysis.period.Period;
+import org.sonar.ce.task.projectanalysis.period.PeriodHolderRule;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
+import org.sonar.ce.task.step.ComputationStep.Context;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ProjectData;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.server.project.Project;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
+
+class PersistReferenceBranchPeriodStepIT {
+
+ private static final String BRANCH_NAME = "feature";
+
+ @RegisterExtension
+ private final PeriodHolderRule periodHolder = new PeriodHolderRule();
+
+ @RegisterExtension
+ private final AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule();
+
+ @RegisterExtension
+ private final TreeRootHolderRule treeRootHolderRule = new TreeRootHolderRule();
+
+ @RegisterExtension
+ private final DbTester db = DbTester.create();
+
+ private final PersistReferenceBranchPeriodStep persistReferenceBranchPeriodStep = new PersistReferenceBranchPeriodStep(
+ periodHolder, analysisMetadataHolder, db.getDbClient(), treeRootHolderRule);
+
+ private ProjectData projectData;
+ private String branchUuid;
+
+ @BeforeEach
+ void setUp() {
+ projectData = db.components().insertPrivateProject();
+ BranchDto branchDto = db.components().insertProjectBranch(projectData.getProjectDto(), branch -> branch.setKey(BRANCH_NAME));
+ branchUuid = branchDto.getUuid();
+
+ analysisMetadataHolder.setProject(new Project(projectData.projectUuid(), projectData.projectKey(), projectData.projectKey(), null, List.of()));
+ analysisMetadataHolder.setBranch(new TestBranch(BRANCH_NAME));
+ periodHolder.setPeriod(new Period(REFERENCE_BRANCH.name(), "main", null));
+ periodHolder.setPeriodOrigin(PeriodOrigin.SCANNER);
+
+ ReportComponent reportComponent = ReportComponent
+ .builder(Component.Type.PROJECT, 1)
+ .setUuid(branchUuid)
+ .setKey(branchDto.getKey())
+ .build();
+ treeRootHolderRule.setRoot(reportComponent);
+ }
+
+ @Test
+ void execute_shouldPersistReferenceBranchPeriod() {
+
+ persistReferenceBranchPeriodStep.execute(mock(Context.class));
+
+ NewCodePeriodDto newCodePeriodDto = db.getDbClient().newCodePeriodDao().selectByBranch(db.getSession(), projectData.projectUuid(), branchUuid)
+ .orElseGet(() -> fail("No new code period found for branch"));
+ assertThat(newCodePeriodDto.getBranchUuid()).isEqualTo(branchUuid);
+ assertThat(newCodePeriodDto.getType()).isEqualTo(REFERENCE_BRANCH);
+ assertThat(newCodePeriodDto.getValue()).isEqualTo("main");
+ }
+
+ @Test
+ void execute_shouldUpdateReferenceBranchPeriod() {
+ db.newCodePeriods().insert(projectData.projectUuid(), branchUuid, REFERENCE_BRANCH, "old_value");
+
+ persistReferenceBranchPeriodStep.execute(mock(Context.class));
+
+ NewCodePeriodDto newCodePeriodDto = db.getDbClient().newCodePeriodDao().selectByBranch(db.getSession(), projectData.projectUuid(), branchUuid)
+ .orElseGet(() -> fail("No new code period found for branch"));
+ assertThat(newCodePeriodDto.getBranchUuid()).isEqualTo(branchUuid);
+ assertThat(newCodePeriodDto.getType()).isEqualTo(REFERENCE_BRANCH);
+ assertThat(newCodePeriodDto.getValue()).isEqualTo("main");
+ }
+
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/TreeRootHolder.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/TreeRootHolder.java
index 89287f0d49a..a37bb86bcb1 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/TreeRootHolder.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/TreeRootHolder.java
@@ -70,14 +70,11 @@ public interface TreeRootHolder {
Component getComponentByUuid(String uuid);
/**
- * Return a component by its batch reference. Returns {@link Optional#empty()} if there's
- * no {@link Component} with the specified reference
+ * Return a component by its scanner reference. Returns {@link Optional#empty()} if there's
+ * no {@link Component} with the specified reference. Note that on PRs, unchanged components are not indexed.
*
* @throws IllegalStateException if the holder is empty (ie. there is no root yet)
- * @deprecated This method was introduced as a quick fix for SONAR-10781. Ultimately one should never manipulate component
- * ref that doesn't exist in the scanner report
*/
- @Deprecated
Optional<Component> getOptionalComponentByRef(int ref);
/**
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/AverageFormula.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/AverageFormula.java
deleted file mode 100644
index 4dab14d2591..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/AverageFormula.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2025 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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.sonar.ce.task.projectanalysis.formula;
-
-import java.util.Optional;
-import org.sonar.ce.task.projectanalysis.measure.Measure;
-
-import static java.util.Objects.requireNonNull;
-
-public class AverageFormula implements Formula<AverageFormula.AverageCounter> {
-
- private final String outputMetricKey;
-
- private final String mainMetric;
- private final String byMetric;
-
- private AverageFormula(Builder builder) {
- this.outputMetricKey = builder.outputMetricKey;
- this.mainMetric = builder.mainMetric;
- this.byMetric = builder.byMetric;
- }
-
- @Override
- public AverageCounter createNewCounter() {
- return new AverageCounter();
- }
-
- @Override
- public Optional<Measure> createMeasure(AverageCounter counter, CreateMeasureContext context) {
- Optional<Double> mainValueOptional = counter.getMainValue();
- Optional<Double> byValueOptional = counter.getByValue();
- if (mainValueOptional.isPresent() && byValueOptional.isPresent()) {
- double mainValue = mainValueOptional.get();
- double byValue = byValueOptional.get();
- if (byValue > 0D) {
- return Optional.of(Measure.newMeasureBuilder().create(mainValue / byValue, context.getMetric().getDecimalScale()));
- }
- }
- return Optional.empty();
- }
-
- @Override
- public String[] getOutputMetricKeys() {
- return new String[] {outputMetricKey};
- }
-
- public static class Builder {
- private String outputMetricKey;
- private String mainMetric;
- private String byMetric;
-
- private Builder() {
- // prevents instantiation outside static method
- }
-
- public static Builder newBuilder() {
- return new Builder();
- }
-
- public Builder setOutputMetricKey(String m) {
- this.outputMetricKey = m;
- return this;
- }
-
- public Builder setMainMetricKey(String m) {
- this.mainMetric = m;
- return this;
- }
-
- public Builder setByMetricKey(String m) {
- this.byMetric = m;
- return this;
- }
-
- public AverageFormula build() {
- requireNonNull(outputMetricKey, "Output metric key cannot be null");
- requireNonNull(mainMetric, "Main metric Key cannot be null");
- requireNonNull(byMetric, "By metric Key cannot be null");
- return new AverageFormula(this);
- }
- }
-
- class AverageCounter implements Counter<AverageCounter> {
-
- private boolean initialized = false;
-
- private double mainValue = 0D;
- private double byValue = 0D;
-
- @Override
- public void aggregate(AverageCounter counter) {
- addValuesIfPresent(counter.getMainValue(), counter.getByValue());
- }
-
- @Override
- public void initialize(CounterInitializationContext context) {
- Optional<Double> mainValueOptional = getDoubleValue(context.getMeasure(mainMetric));
- Optional<Double> byValueOptional = getDoubleValue(context.getMeasure(byMetric));
- addValuesIfPresent(mainValueOptional, byValueOptional);
- }
-
- private void addValuesIfPresent(Optional<Double> counterMainValue, Optional<Double> counterByValue) {
- if (counterMainValue.isPresent() && counterByValue.isPresent()) {
- initialized = true;
- mainValue += counterMainValue.get();
- byValue += counterByValue.get();
- }
- }
-
- public Optional<Double> getMainValue() {
- return getValue(mainValue);
- }
-
- public Optional<Double> getByValue() {
- return getValue(byValue);
- }
-
- private Optional<Double> getValue(double value) {
- if (initialized) {
- return Optional.of(value);
- }
- return Optional.empty();
- }
-
- private Optional<Double> getDoubleValue(Optional<Measure> measureOptional) {
- if (!measureOptional.isPresent()) {
- return Optional.empty();
- }
- Measure measure = measureOptional.get();
- switch (measure.getValueType()) {
- case DOUBLE:
- return Optional.of(measure.getDoubleValue());
- case LONG:
- return Optional.of((double) measure.getLongValue());
- case INT:
- return Optional.of((double) measure.getIntValue());
- case NO_VALUE:
- return Optional.empty();
- default:
- throw new IllegalArgumentException(String.format("Measure of type '%s' are not supported", measure.getValueType().name()));
- }
- }
- }
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormula.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormula.java
deleted file mode 100644
index d0d0bacd0bc..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormula.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2025 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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.sonar.ce.task.projectanalysis.formula;
-
-import java.util.Optional;
-import org.sonar.api.ce.measure.RangeDistributionBuilder;
-import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
-import org.sonar.ce.task.projectanalysis.measure.Measure;
-
-import static java.util.Objects.requireNonNull;
-
-public class DistributionFormula implements Formula<DistributionFormula.DistributionCounter> {
-
- private final String metricKey;
-
- public DistributionFormula(String metricKey) {
- this.metricKey = requireNonNull(metricKey, "Metric key cannot be null");
- }
-
- @Override
- public DistributionCounter createNewCounter() {
- return new DistributionCounter();
- }
-
- @Override
- public Optional<Measure> createMeasure(DistributionCounter counter, CreateMeasureContext context) {
- Component.Type componentType = context.getComponent().getType();
- Optional<String> value = counter.getValue();
- if (value.isPresent() && CrawlerDepthLimit.LEAVES.isDeeperThan(componentType)) {
- return Optional.of(Measure.newMeasureBuilder().create(value.get()));
- }
- return Optional.empty();
- }
-
- @Override
- public String[] getOutputMetricKeys() {
- return new String[] {metricKey};
- }
-
- class DistributionCounter implements Counter<DistributionCounter> {
-
- private final RangeDistributionBuilder distribution = new RangeDistributionBuilder();
- private boolean initialized = false;
-
- @Override
- public void aggregate(DistributionCounter counter) {
- Optional<String> value = counter.getValue();
- if (value.isPresent()) {
- initialized = true;
- distribution.add(value.get());
- }
- }
-
- @Override
- public void initialize(CounterInitializationContext context) {
- Optional<Measure> measureOptional = context.getMeasure(metricKey);
- String data = measureOptional.isPresent() ? measureOptional.get().getData() : null;
- if (data != null) {
- initialized = true;
- distribution.add(data);
- }
- }
-
- public Optional<String> getValue() {
- if (initialized) {
- return Optional.ofNullable(distribution.build());
- }
- return Optional.empty();
- }
- }
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
index dc006242431..8b62a6abaae 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
@@ -345,7 +345,8 @@ public class TrackerRawInputFactory {
private Optional<DbIssues.Location> convertLocation(ScannerReport.IssueLocation source) {
DbIssues.Location.Builder target = DbIssues.Location.newBuilder();
if (source.getComponentRef() != 0 && source.getComponentRef() != component.getReportAttributes().getRef()) {
- // SONAR-10781 Component might not exist because on PR, only changed components are included in the report
+ // Component might not exist because on PR, only changed components are included in the component tree
+ // See in BuildComponentTreeStep the call to buildChangedComponentTreeRoot
Optional<Component> optionalComponent = treeRootHolder.getOptionalComponentByRef(source.getComponentRef());
if (optionalComponent.isEmpty()) {
return Optional.empty();
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationCheck.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationCheck.java
index 27ffc039b71..9863836e1d0 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationCheck.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationCheck.java
@@ -52,6 +52,8 @@ public interface PostMeasuresComputationCheck {
*/
String getProjectUuid();
+ String getAnalysisUuid();
+
Branch getBranch();
ScannerReportReader getReportReader();
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStep.java
index 4ce9187cc70..fb40cf4ed7c 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStep.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStep.java
@@ -77,6 +77,11 @@ public class PostMeasuresComputationChecksStep implements ComputationStep {
}
@Override
+ public String getAnalysisUuid() {
+ return analysisMetadataHolder.getUuid();
+ }
+
+ @Override
public Branch getBranch() {
return analysisMetadataHolder.getBranch();
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepository.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepository.java
index 6dc8dd686d9..751bf23ed8d 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepository.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepository.java
@@ -26,9 +26,11 @@ public interface MetricRepository {
/**
* Gets the {@link Metric} with the specific key.
- * <p>Since it does not make sense to encounter a reference (ie. a key) to a Metric during processing of
+ * <p>Since it <i>mostly</i> does not make sense to encounter a reference (ie. a key) to a Metric during processing of
* a new analysis and not finding it in DB (metrics are never deleted), this method will throw an
* IllegalStateException if the metric with the specified key can not be found.</p>
+ * <p>Core extensions that add their own metrics on commercial versions may not be available everywhere.
+ * Use getOptionalByKey if you need to work with a metric that may not exist in all environments.</p>
*
* @throws IllegalStateException if no Metric with the specified key is found
* @throws NullPointerException if the specified key is {@code null}
@@ -36,6 +38,14 @@ public interface MetricRepository {
Metric getByKey(String key);
/**
+ * Gets the {@link Metric} with the specific key if it exists. Useful if working with a metric that may
+ * not exist in all versions of SonarQube Server.
+ *
+ * @throws NullPointerException if the specified key is {@code null}
+ */
+ Optional<Metric> getOptionalByKey(String key);
+
+ /**
* Gets the {@link Metric} with the specific uuid.
*
* @throws IllegalStateException if no Metric with the specified uuid is found
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImpl.java
index 6078d8d15bc..1a6a8a16bcf 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImpl.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryImpl.java
@@ -70,6 +70,16 @@ public class MetricRepositoryImpl implements MetricRepository, Startable {
}
@Override
+ public Optional<Metric> getOptionalByKey(String key) {
+ requireNonNull(key);
+ verifyMetricsInitialized();
+
+ Metric res = this.metricsByKey.get(key);
+
+ return Optional.ofNullable(res);
+ }
+
+ @Override
public Metric getByUuid(String uuid) {
return getOptionalByUuid(uuid)
.orElseThrow(() -> new IllegalStateException(String.format("Metric with uuid '%s' does not exist", uuid)));
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java
index 1705c9fb3bb..9fe7dadb9f5 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java
@@ -51,4 +51,10 @@ public interface PeriodHolder {
*/
Period getPeriod();
+ /**
+ * Retrieve the context from which this period is coming from. For example, it can be coming from a scanner parameter, a global setting, etc.
+ * See {@link PeriodOrigin} for the possible values.
+ */
+ PeriodOrigin getPeriodOrigin();
+
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java
index 86ea07a18e2..2150f3f5539 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java
@@ -29,6 +29,7 @@ public class PeriodHolderImpl implements PeriodHolder {
@CheckForNull
private Period period = null;
private boolean initialized = false;
+ private PeriodOrigin periodOrigin = null;
/**
* Initializes the periods in the holder.
@@ -41,6 +42,10 @@ public class PeriodHolderImpl implements PeriodHolder {
this.initialized = true;
}
+ public void setPeriodOrigin(PeriodOrigin periodOrigin) {
+ this.periodOrigin = periodOrigin;
+ }
+
@Override
public boolean hasPeriod() {
checkHolderIsInitialized();
@@ -60,6 +65,11 @@ public class PeriodHolderImpl implements PeriodHolder {
return period;
}
+ @Override
+ public PeriodOrigin getPeriodOrigin() {
+ return periodOrigin;
+ }
+
private void checkHolderIsInitialized() {
checkState(initialized, "Period have not been initialized yet");
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java
new file mode 100644
index 00000000000..6e2847fe480
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.ce.task.projectanalysis.period;
+
+public enum PeriodOrigin {
+ SCANNER,
+ SETTINGS
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
index 8d9026c42af..84b93149e2c 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java
@@ -20,15 +20,13 @@
package org.sonar.ce.task.projectanalysis.step;
import java.util.Optional;
-import org.sonar.api.utils.System2;
-import org.sonar.ce.task.log.CeTaskMessages;
-import org.sonar.ce.task.log.CeTaskMessages.Message;
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
import org.sonar.ce.task.projectanalysis.period.NewCodePeriodResolver;
import org.sonar.ce.task.projectanalysis.period.Period;
import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
import org.sonar.ce.task.projectanalysis.period.PeriodHolderImpl;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
import org.sonar.ce.task.step.ComputationStep;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
@@ -53,19 +51,15 @@ public class LoadPeriodsStep implements ComputationStep {
private final PeriodHolderImpl periodsHolder;
private final DbClient dbClient;
private final NewCodePeriodResolver resolver;
- private final CeTaskMessages ceTaskMessages;
- private final System2 system2;
public LoadPeriodsStep(AnalysisMetadataHolder analysisMetadataHolder, NewCodePeriodDao newCodePeriodDao, TreeRootHolder treeRootHolder,
- PeriodHolderImpl periodsHolder, DbClient dbClient, NewCodePeriodResolver resolver, CeTaskMessages ceTaskMessages, System2 system2) {
+ PeriodHolderImpl periodsHolder, DbClient dbClient, NewCodePeriodResolver resolver) {
this.analysisMetadataHolder = analysisMetadataHolder;
this.newCodePeriodDao = newCodePeriodDao;
this.treeRootHolder = treeRootHolder;
this.periodsHolder = periodsHolder;
this.dbClient = dbClient;
this.resolver = resolver;
- this.ceTaskMessages = ceTaskMessages;
- this.system2 = system2;
}
@Override
@@ -89,6 +83,8 @@ public class LoadPeriodsStep implements ComputationStep {
.map(b -> new NewCodePeriodDto().setType(REFERENCE_BRANCH).setValue(b))
.orElse(null);
+ PeriodOrigin periodOrigin = newCodePeriod == null ? PeriodOrigin.SETTINGS : PeriodOrigin.SCANNER;
+
try (DbSession dbSession = dbClient.openSession(false)) {
Optional<NewCodePeriodDto> branchSpecificSetting = getBranchSetting(dbSession, projectUuid, branchUuid);
@@ -102,13 +98,11 @@ public class LoadPeriodsStep implements ComputationStep {
periodsHolder.setPeriod(null);
return;
}
- } else if (branchSpecificSetting.isPresent()) {
- ceTaskMessages.add(new Message("A scanner parameter is defining a new code reference branch, but this conflicts with the New Code Period"
- + " setting of your branch. Please check your project configuration. You should use either one or the other but not both.", system2.now()));
}
Period period = resolver.resolve(dbSession, branchUuid, newCodePeriod, projectVersion);
periodsHolder.setPeriod(period);
+ periodsHolder.setPeriodOrigin(periodOrigin);
}
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java
new file mode 100644
index 00000000000..96f471946ae
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.ce.task.projectanalysis.step;
+
+import java.util.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
+import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
+import org.sonar.ce.task.step.ComputationStep;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+
+public class PersistReferenceBranchPeriodStep implements ComputationStep {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PersistReferenceBranchPeriodStep.class);
+
+ private final PeriodHolder periodHolder;
+ private final AnalysisMetadataHolder analysisMetadataHolder;
+ private final DbClient dbClient;
+ private final TreeRootHolder treeRootHolder;
+
+ public PersistReferenceBranchPeriodStep(PeriodHolder periodHolder, AnalysisMetadataHolder analysisMetadataHolder, DbClient dbClient, TreeRootHolder treeRootHolder) {
+ this.periodHolder = periodHolder;
+ this.analysisMetadataHolder = analysisMetadataHolder;
+ this.dbClient = dbClient;
+ this.treeRootHolder = treeRootHolder;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Persist or update reference branch new code period";
+ }
+
+ @Override
+ public void execute(Context context) {
+ if (shouldExecute()) {
+ executePersistPeriodStep();
+ }
+ }
+
+ private boolean shouldExecute() {
+ return analysisMetadataHolder.isBranch() && periodHolder.hasPeriod()
+ && periodHolder.getPeriodOrigin() == PeriodOrigin.SCANNER;
+ }
+
+ void executePersistPeriodStep() {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ String projectUuid = analysisMetadataHolder.getProject().getUuid();
+ String branchUuid = treeRootHolder.getRoot().getUuid();
+
+ dbClient.newCodePeriodDao()
+ .selectByBranch(dbSession, projectUuid, branchUuid)
+ .ifPresentOrElse(
+ existingNewCodePeriod -> updateNewCodePeriodIfNeeded(dbSession, existingNewCodePeriod),
+ () -> createNewCodePeriod(dbSession, branchUuid)
+ );
+ }
+ }
+
+ private void updateNewCodePeriodIfNeeded(DbSession dbSession, NewCodePeriodDto newCodePeriodDto) {
+ if (shouldUpdateNewCodePeriod(newCodePeriodDto)) {
+ LOGGER.debug("Updating reference branch new code period '{}' for project '{}' and branch '{}'",
+ periodHolder.getPeriod().getModeParameter(), analysisMetadataHolder.getProject().getName(), analysisMetadataHolder.getBranch().getName());
+ newCodePeriodDto.setValue(periodHolder.getPeriod().getModeParameter());
+ newCodePeriodDto.setType(NewCodePeriodType.REFERENCE_BRANCH);
+ dbClient.newCodePeriodDao().update(dbSession, newCodePeriodDto);
+ dbSession.commit();
+ }
+ }
+
+ private boolean shouldUpdateNewCodePeriod(NewCodePeriodDto existingNewCodePeriod) {
+ return existingNewCodePeriod.getType() != NewCodePeriodType.REFERENCE_BRANCH
+ || !Objects.equals(existingNewCodePeriod.getValue(), periodHolder.getPeriod().getModeParameter());
+ }
+
+ private void createNewCodePeriod(DbSession dbSession, String branchUuid) {
+ LOGGER.debug("Persisting reference branch new code period '{}' for project '{}' and branch '{}'",
+ periodHolder.getPeriod().getModeParameter(), analysisMetadataHolder.getProject().getName(), analysisMetadataHolder.getBranch().getName());
+ dbClient.newCodePeriodDao().insert(dbSession, buildNewCodePeriodDto(branchUuid));
+ dbSession.commit();
+ }
+
+ private NewCodePeriodDto buildNewCodePeriodDto(String branchUuid) {
+ return new NewCodePeriodDto()
+ .setProjectUuid(analysisMetadataHolder.getProject().getUuid())
+ .setBranchUuid(branchUuid)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(periodHolder.getPeriod().getModeParameter());
+ }
+
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
index 8cbcbbf421a..b121df6e657 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
@@ -106,6 +106,7 @@ public class ReportComputationSteps extends AbstractComputationSteps {
// Persist data
PersistScannerAnalysisCacheStep.class,
PersistComponentsStep.class,
+ PersistReferenceBranchPeriodStep.class,
PersistAnalysisStep.class,
PersistAnalysisPropertiesStep.class,
PersistProjectMeasuresStep.class,
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaExecutionTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaExecutionTest.java
deleted file mode 100644
index 519312ef9cb..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaExecutionTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2025 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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.sonar.ce.task.projectanalysis.formula;
-
-import com.google.common.collect.Lists;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.measures.CoreMetrics;
-import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.PathAwareCrawler;
-import org.sonar.ce.task.projectanalysis.component.ReportComponent;
-import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
-import org.sonar.ce.task.projectanalysis.measure.MeasureRepositoryRule;
-import org.sonar.ce.task.projectanalysis.metric.MetricRepositoryRule;
-import org.sonar.ce.task.projectanalysis.period.PeriodHolderRule;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.api.measures.CoreMetrics.COMPLEXITY_IN_FUNCTIONS_KEY;
-import static org.sonar.api.measures.CoreMetrics.FUNCTIONS_KEY;
-import static org.sonar.api.measures.CoreMetrics.FUNCTION_COMPLEXITY_KEY;
-import static org.sonar.ce.task.projectanalysis.component.Component.Type.DIRECTORY;
-import static org.sonar.ce.task.projectanalysis.component.Component.Type.PROJECT;
-import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder;
-import static org.sonar.ce.task.projectanalysis.measure.Measure.newMeasureBuilder;
-import static org.sonar.ce.task.projectanalysis.measure.MeasureRepoEntry.entryOf;
-import static org.sonar.ce.task.projectanalysis.measure.MeasureRepoEntry.toEntries;
-
-public class AverageFormulaExecutionTest {
-
- @Rule
- public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
- @Rule
- public MetricRepositoryRule metricRepository = new MetricRepositoryRule()
- .add(CoreMetrics.FUNCTION_COMPLEXITY)
- .add(CoreMetrics.COMPLEXITY_IN_FUNCTIONS)
- .add(CoreMetrics.FUNCTIONS);
- @Rule
- public MeasureRepositoryRule measureRepository = MeasureRepositoryRule.create(treeRootHolder, metricRepository);
- @Rule
- public PeriodHolderRule periodsHolder = new PeriodHolderRule();
-
- private FormulaExecutorComponentVisitor underTest;
-
- @Before
- public void setUp() {
- underTest = FormulaExecutorComponentVisitor.newBuilder(metricRepository, measureRepository)
- .buildFor(Lists.newArrayList(
- AverageFormula.Builder.newBuilder()
- .setOutputMetricKey(FUNCTION_COMPLEXITY_KEY)
- .setMainMetricKey(COMPLEXITY_IN_FUNCTIONS_KEY)
- .setByMetricKey(FUNCTIONS_KEY)
- .build()));
- }
-
- @Test
- public void add_measures() {
- ReportComponent project = builder(PROJECT, 1)
- .addChildren(
- builder(DIRECTORY, 111)
- .addChildren(
- builder(Component.Type.FILE, 1111).build(),
- builder(Component.Type.FILE, 1112).build())
- .build(),
- builder(DIRECTORY, 121)
- .addChildren(
- builder(Component.Type.FILE, 1211).build())
- .build())
- .build();
-
- treeRootHolder.setRoot(project);
-
- measureRepository.addRawMeasure(1111, COMPLEXITY_IN_FUNCTIONS_KEY, newMeasureBuilder().create(5));
- measureRepository.addRawMeasure(1111, FUNCTIONS_KEY, newMeasureBuilder().create(2));
-
- measureRepository.addRawMeasure(1112, COMPLEXITY_IN_FUNCTIONS_KEY, newMeasureBuilder().create(1));
- measureRepository.addRawMeasure(1112, FUNCTIONS_KEY, newMeasureBuilder().create(1));
-
- measureRepository.addRawMeasure(1211, COMPLEXITY_IN_FUNCTIONS_KEY, newMeasureBuilder().create(9));
- measureRepository.addRawMeasure(1211, FUNCTIONS_KEY, newMeasureBuilder().create(2));
-
- new PathAwareCrawler<>(underTest).visit(project);
-
- assertThat(toEntries(measureRepository.getAddedRawMeasures(1))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(3d, 1)));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(111))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(2d, 1)));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(1111))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(2.5d, 1)));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(1112))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(1d, 1)));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(121))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(4.5d, 1)));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(1211))).containsOnly(entryOf(FUNCTION_COMPLEXITY_KEY, newMeasureBuilder().create(4.5d, 1)));
- }
-
- @Test
- public void not_add_measures_when_no_data_on_file() {
- ReportComponent project = builder(PROJECT, 1)
- .addChildren(
- builder(DIRECTORY, 111)
- .addChildren(
- builder(Component.Type.FILE, 1111).build())
- .build())
- .build();
-
- treeRootHolder.setRoot(project);
-
- new PathAwareCrawler<>(underTest).visit(project);
-
- assertThat(measureRepository.getAddedRawMeasures(1)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(111)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(1111)).isEmpty();
- }
-
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaTest.java
deleted file mode 100644
index 7d830eac3d5..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/AverageFormulaTest.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2025 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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.sonar.ce.task.projectanalysis.formula;
-
-import java.util.Optional;
-import org.junit.Test;
-import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.ReportComponent;
-import org.sonar.ce.task.projectanalysis.measure.Measure;
-import org.sonar.ce.task.projectanalysis.metric.Metric;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.api.measures.CoreMetrics.COMPLEXITY_IN_FUNCTIONS_KEY;
-import static org.sonar.api.measures.CoreMetrics.FUNCTIONS_KEY;
-import static org.sonar.api.measures.CoreMetrics.FUNCTION_COMPLEXITY_KEY;
-import static org.sonar.ce.task.projectanalysis.formula.AverageFormula.Builder;
-
-public class AverageFormulaTest {
-
- private static final AverageFormula BASIC_AVERAGE_FORMULA = Builder.newBuilder()
- .setOutputMetricKey(FUNCTION_COMPLEXITY_KEY)
- .setMainMetricKey(COMPLEXITY_IN_FUNCTIONS_KEY)
- .setByMetricKey(FUNCTIONS_KEY)
- .build();
-
- CounterInitializationContext counterInitializationContext = mock(CounterInitializationContext.class);
- CreateMeasureContext createMeasureContext = new DumbCreateMeasureContext(
- ReportComponent.builder(Component.Type.PROJECT, 1).build(), mock(Metric.class));
-
-
- @Test
- public void fail_with_NPE_when_building_formula_without_output_metric() {
- assertThatThrownBy(() -> {
- Builder.newBuilder()
- .setOutputMetricKey(null)
- .setMainMetricKey(COMPLEXITY_IN_FUNCTIONS_KEY)
- .setByMetricKey(FUNCTIONS_KEY)
- .build();
- })
- .isInstanceOf(NullPointerException.class)
- .hasMessage("Output metric key cannot be null");
- }
-
- @Test
- public void fail_with_NPE_when_building_formula_without_main_metric() {
- assertThatThrownBy(() -> {
- Builder.newBuilder()
- .setOutputMetricKey(FUNCTION_COMPLEXITY_KEY)
- .setMainMetricKey(null)
- .setByMetricKey(FUNCTIONS_KEY)
- .build();
- })
- .isInstanceOf(NullPointerException.class)
- .hasMessage("Main metric Key cannot be null");
- }
-
- @Test
- public void fail_with_NPE_when_building_formula_without_by_metric() {
- assertThatThrownBy(() -> {
- Builder.newBuilder()
- .setOutputMetricKey(FUNCTION_COMPLEXITY_KEY)
- .setMainMetricKey(COMPLEXITY_IN_FUNCTIONS_KEY)
- .setByMetricKey(null)
- .build();
- })
- .isInstanceOf(NullPointerException.class)
- .hasMessage("By metric Key cannot be null");
- }
-
- @Test
- public void check_new_counter_class() {
- assertThat(BASIC_AVERAGE_FORMULA.createNewCounter().getClass()).isEqualTo(AverageFormula.AverageCounter.class);
- }
-
- @Test
- public void check_output_metric_key_is_function_complexity_key() {
- assertThat(BASIC_AVERAGE_FORMULA.getOutputMetricKeys()).containsOnly(FUNCTION_COMPLEXITY_KEY);
- }
-
- @Test
- public void create_measure_when_counter_is_aggregated_from_context() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10d);
- addMeasure(FUNCTIONS_KEY, 2d);
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext).get().getDoubleValue()).isEqualTo(5d);
- }
-
- @Test
- public void create_measure_when_counter_is_aggregated_from_another_counter() {
- AverageFormula.AverageCounter anotherCounter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10d);
- addMeasure(FUNCTIONS_KEY, 2d);
- anotherCounter.initialize(counterInitializationContext);
-
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- counter.aggregate(anotherCounter);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext).get().getDoubleValue()).isEqualTo(5d);
- }
-
- @Test
- public void create_double_measure() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10d);
- addMeasure(FUNCTIONS_KEY, 2d);
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext).get().getDoubleValue()).isEqualTo(5d);
- }
-
- @Test
- public void create_integer_measure() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10);
- addMeasure(FUNCTIONS_KEY, 2);
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext).get().getDoubleValue()).isEqualTo(5);
- }
-
- @Test
- public void create_long_measure() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10L);
- addMeasure(FUNCTIONS_KEY, 2L);
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext).get().getDoubleValue()).isEqualTo(5L);
- }
-
- @Test
- public void not_create_measure_when_aggregated_measure_has_no_value() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10L);
- when(counterInitializationContext.getMeasure(FUNCTIONS_KEY)).thenReturn(Optional.of(Measure.newMeasureBuilder().createNoValue()));
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext)).isNotPresent();
- }
-
- @Test
- public void fail_with_IAE_when_aggregate_from_component_and_context_with_not_numeric_measures() {
- assertThatThrownBy(() -> {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10L);
- when(counterInitializationContext.getMeasure(FUNCTIONS_KEY)).thenReturn(Optional.of(Measure.newMeasureBuilder().create("data")));
- counter.initialize(counterInitializationContext);
-
- BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext);
- })
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Measure of type 'STRING' are not supported");
- }
-
- @Test
- public void no_measure_created_when_counter_has_no_value() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- when(counterInitializationContext.getMeasure(anyString())).thenReturn(Optional.empty());
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext)).isNotPresent();
- }
-
- @Test
- public void not_create_measure_when_only_one_measure() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10L);
- when(counterInitializationContext.getMeasure(FUNCTIONS_KEY)).thenReturn(Optional.empty());
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext)).isNotPresent();
- }
-
- @Test
- public void not_create_measure_when_by_value_is_zero() {
- AverageFormula.AverageCounter counter = BASIC_AVERAGE_FORMULA.createNewCounter();
- addMeasure(COMPLEXITY_IN_FUNCTIONS_KEY, 10d);
- addMeasure(FUNCTIONS_KEY, 0d);
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_AVERAGE_FORMULA.createMeasure(counter, createMeasureContext)).isNotPresent();
- }
-
- private void addMeasure(String metricKey, double value) {
- when(counterInitializationContext.getMeasure(metricKey)).thenReturn(Optional.of(Measure.newMeasureBuilder().create(value, 1)));
- }
-
- private void addMeasure(String metricKey, int value) {
- when(counterInitializationContext.getMeasure(metricKey)).thenReturn(Optional.of(Measure.newMeasureBuilder().create(value)));
- }
-
- private void addMeasure(String metricKey, long value) {
- when(counterInitializationContext.getMeasure(metricKey)).thenReturn(Optional.of(Measure.newMeasureBuilder().create(value)));
- }
-
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaExecutionTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaExecutionTest.java
deleted file mode 100644
index a406c0dc6f5..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaExecutionTest.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2025 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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.sonar.ce.task.projectanalysis.formula;
-
-import com.google.common.collect.Lists;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.measures.CoreMetrics;
-import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.PathAwareCrawler;
-import org.sonar.ce.task.projectanalysis.component.ReportComponent;
-import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
-import org.sonar.ce.task.projectanalysis.measure.MeasureRepositoryRule;
-import org.sonar.ce.task.projectanalysis.metric.MetricRepositoryRule;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.api.measures.CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION_KEY;
-import static org.sonar.ce.task.projectanalysis.component.Component.Type.DIRECTORY;
-import static org.sonar.ce.task.projectanalysis.component.Component.Type.PROJECT;
-import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder;
-import static org.sonar.ce.task.projectanalysis.measure.Measure.newMeasureBuilder;
-import static org.sonar.ce.task.projectanalysis.measure.MeasureRepoEntry.entryOf;
-import static org.sonar.ce.task.projectanalysis.measure.MeasureRepoEntry.toEntries;
-
-public class DistributionFormulaExecutionTest {
-
- @Rule
- public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
- @Rule
- public MetricRepositoryRule metricRepository = new MetricRepositoryRule().add(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION);
- @Rule
- public MeasureRepositoryRule measureRepository = MeasureRepositoryRule.create(treeRootHolder, metricRepository);
-
- FormulaExecutorComponentVisitor underTest;
-
- @Before
- public void setUp() throws Exception {
- underTest = FormulaExecutorComponentVisitor.newBuilder(metricRepository, measureRepository)
- .buildFor(Lists.newArrayList(new DistributionFormula(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY)));
- }
-
- @Test
- public void add_measures() {
- ReportComponent project = builder(PROJECT, 1)
- .addChildren(
- builder(DIRECTORY, 11)
- .addChildren(
- builder(DIRECTORY, 111)
- .addChildren(
- builder(Component.Type.FILE, 1111).build(),
- builder(Component.Type.FILE, 1112).build())
- .build())
- .build(),
- builder(DIRECTORY, 12)
- .addChildren(
- builder(DIRECTORY, 121)
- .addChildren(
- builder(Component.Type.FILE, 1211).build())
- .build())
- .build())
- .build();
-
- treeRootHolder.setRoot(project);
-
- measureRepository.addRawMeasure(1111, FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=3;3.5=5;6.5=9"));
- measureRepository.addRawMeasure(1112, FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=0;3.5=2;6.5=1"));
- measureRepository.addRawMeasure(1211, FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=1;3.5=3;6.5=2"));
-
- new PathAwareCrawler<>(underTest).visit(project);
-
- assertThat(toEntries(measureRepository.getAddedRawMeasures(1))).containsOnly(entryOf(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=4;3.5=10;6.5=12")));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(11))).containsOnly(entryOf(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=3;3.5=7;6.5=10")));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(111))).containsOnly(entryOf(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=3;3.5=7;6.5=10")));
- assertThat(measureRepository.getAddedRawMeasures(1111)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(1112)).isEmpty();
- assertThat(toEntries(measureRepository.getAddedRawMeasures(12))).containsOnly(entryOf(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=1;3.5=3;6.5=2")));
- assertThat(toEntries(measureRepository.getAddedRawMeasures(121))).containsOnly(entryOf(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, newMeasureBuilder().create("0.5=1;3.5=3;6.5=2")));
- assertThat(measureRepository.getAddedRawMeasures(1211)).isEmpty();
- }
-
- @Test
- public void not_add_measures_when_no_data_on_file() {
- ReportComponent project = builder(PROJECT, 1)
- .addChildren(
- builder(DIRECTORY, 11)
- .addChildren(
- builder(DIRECTORY, 111)
- .addChildren(
- builder(Component.Type.FILE, 1111).build())
- .build())
- .build())
- .build();
-
- treeRootHolder.setRoot(project);
-
- new PathAwareCrawler<>(underTest).visit(project);
-
- assertThat(measureRepository.getAddedRawMeasures(1)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(11)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(111)).isEmpty();
- assertThat(measureRepository.getAddedRawMeasures(1111)).isEmpty();
- }
-
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaTest.java
deleted file mode 100644
index 89df9c43f79..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/formula/DistributionFormulaTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2025 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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.sonar.ce.task.projectanalysis.formula;
-
-import java.util.Optional;
-import org.junit.Test;
-import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.ReportComponent;
-import org.sonar.ce.task.projectanalysis.measure.Measure;
-import org.sonar.ce.task.projectanalysis.metric.Metric;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.api.measures.CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION_KEY;
-
-public class DistributionFormulaTest {
-
- private static final DistributionFormula BASIC_DISTRIBUTION_FORMULA = new DistributionFormula(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY);
-
-
- CounterInitializationContext counterInitializationContext = mock(CounterInitializationContext.class);
- CreateMeasureContext projectCreateMeasureContext = new DumbCreateMeasureContext(
- ReportComponent.builder(Component.Type.PROJECT, 1).build(), mock(Metric.class));
- CreateMeasureContext fileCreateMeasureContext = new DumbCreateMeasureContext(
- ReportComponent.builder(Component.Type.FILE, 1).build(), mock(Metric.class));
-
- @Test
- public void check_new_counter_class() {
- assertThat(BASIC_DISTRIBUTION_FORMULA.createNewCounter().getClass()).isEqualTo(DistributionFormula.DistributionCounter.class);
- }
-
- @Test
- public void fail_with_NPE_when_creating_counter_with_null_metric() {
- assertThatThrownBy(() -> new DistributionFormula(null))
- .isInstanceOf(NullPointerException.class)
- .hasMessage("Metric key cannot be null");
- }
-
- @Test
- public void check_output_metric_key_is_function_complexity_distribution() {
- assertThat(BASIC_DISTRIBUTION_FORMULA.getOutputMetricKeys()).containsOnly(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY);
- }
-
- @Test
- public void create_measure() {
- DistributionFormula.DistributionCounter counter = BASIC_DISTRIBUTION_FORMULA.createNewCounter();
- addMeasure(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, "0=3;3=7;6=10");
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_DISTRIBUTION_FORMULA.createMeasure(counter, projectCreateMeasureContext).get().getData()).isEqualTo("0=3;3=7;6=10");
- }
-
- @Test
- public void create_measure_when_counter_is_aggregating_from_another_counter() {
- DistributionFormula.DistributionCounter anotherCounter = BASIC_DISTRIBUTION_FORMULA.createNewCounter();
- addMeasure(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, "0=3;3=7;6=10");
- anotherCounter.initialize(counterInitializationContext);
-
- DistributionFormula.DistributionCounter counter = BASIC_DISTRIBUTION_FORMULA.createNewCounter();
- counter.aggregate(anotherCounter);
-
- assertThat(BASIC_DISTRIBUTION_FORMULA.createMeasure(counter, projectCreateMeasureContext).get().getData()).isEqualTo("0=3;3=7;6=10");
- }
-
- @Test
- public void create_no_measure_when_no_value() {
- DistributionFormula.DistributionCounter counter = BASIC_DISTRIBUTION_FORMULA.createNewCounter();
- when(counterInitializationContext.getMeasure(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY)).thenReturn(Optional.empty());
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_DISTRIBUTION_FORMULA.createMeasure(counter, projectCreateMeasureContext)).isNotPresent();
- }
-
- @Test
- public void not_create_measure_when_on_file() {
- DistributionFormula.DistributionCounter counter = BASIC_DISTRIBUTION_FORMULA.createNewCounter();
- addMeasure(FUNCTION_COMPLEXITY_DISTRIBUTION_KEY, "0=3;3=7;6=10");
- counter.initialize(counterInitializationContext);
-
- assertThat(BASIC_DISTRIBUTION_FORMULA.createMeasure(counter, fileCreateMeasureContext)).isNotPresent();
- }
-
- private void addMeasure(String metricKey, String value) {
- when(counterInitializationContext.getMeasure(metricKey)).thenReturn(Optional.of(Measure.newMeasureBuilder().create(value)));
- }
-
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStepTest.java
index eb80fcfb603..e5ea57b4333 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStepTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/PostMeasuresComputationChecksStepTest.java
@@ -19,6 +19,7 @@
*/
package org.sonar.ce.task.projectanalysis.measure;
+import java.util.UUID;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -40,6 +41,7 @@ import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static org.sonar.api.measures.CoreMetrics.NCLOC;
import static org.sonar.ce.task.projectanalysis.component.ReportComponent.DUMB_PROJECT;
import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
@@ -105,6 +107,19 @@ public class PostMeasuresComputationChecksStepTest {
}
@Test
+ public void whenOnCheck_thenAnalysisUuidIsPresent() {
+ String analysisUuid = "analysisUuid";
+ PostMeasuresComputationCheck check = mock(PostMeasuresComputationCheck.class);
+ analysisMetadataHolder.setUuid(analysisUuid);
+
+ newStep(check).execute(new TestComputationStepContext());
+
+ ArgumentCaptor<Context> contextArgumentCaptor = ArgumentCaptor.forClass(Context.class);
+ verify(check).onCheck(contextArgumentCaptor.capture());
+ assertThat(contextArgumentCaptor.getValue().getAnalysisUuid()).isEqualTo(analysisUuid);
+ }
+
+ @Test
public void do_nothing_if_no_extensions() {
// no failure
newStep().execute(new TestComputationStepContext());
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java
index f47424267a9..7c9b454e8c2 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java
@@ -73,4 +73,14 @@ public class PeriodHolderRule implements TestRule, PeriodHolder, AfterEachCallba
return delegate.getPeriod();
}
+ @Override
+ public PeriodOrigin getPeriodOrigin() {
+ return delegate.getPeriodOrigin();
+ }
+
+ public PeriodHolderRule setPeriodOrigin(PeriodOrigin periodOrigin) {
+ delegate.setPeriodOrigin(periodOrigin);
+ return this;
+ }
+
}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java
new file mode 100644
index 00000000000..84c3cc6d42d
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java
@@ -0,0 +1,194 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.ce.task.projectanalysis.step;
+
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.ArgumentCaptor;
+import org.slf4j.event.Level;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.analysis.TestBranch;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
+import org.sonar.ce.task.projectanalysis.period.Period;
+import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
+import org.sonar.ce.task.projectanalysis.period.PeriodOrigin;
+import org.sonar.ce.task.step.ComputationStep;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.newcodeperiod.NewCodePeriodDao;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+import org.sonar.server.project.Project;
+
+import static java.util.Collections.emptyList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.slf4j.event.Level.DEBUG;
+
+class PersistReferenceBranchPeriodStepTest {
+
+ private static final String MAIN_BRANCH = "main";
+ private static final String FEATURE_BRANCH = "feature";
+ private static final String BRANCH_UUID = "branch-uuid";
+ private static final String PROJECT_NAME = "project-name";
+ private static final String PROJECT_UUID = "project-uuid";
+
+ private final PeriodHolder periodHolder = mock(PeriodHolder.class);
+
+ private final AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class);
+
+ private final DbClient dbClient = mock(DbClient.class);
+
+ private final TreeRootHolder treeRootHolder = mock(TreeRootHolder.class);
+
+ @RegisterExtension
+ private final LogTesterJUnit5 logs = new LogTesterJUnit5().setLevel(Level.DEBUG);
+
+ private final PersistReferenceBranchPeriodStep persistReferenceBranchPeriodStep = new PersistReferenceBranchPeriodStep(
+ periodHolder, analysisMetadataHolder, dbClient, treeRootHolder);
+
+ private final DbSession dbSession = mock(DbSession.class);
+ private final NewCodePeriodDao newCodePeriodeDao = mock(NewCodePeriodDao.class);
+
+ private final ComputationStep.Context context = mock(ComputationStep.Context.class);
+
+ @BeforeEach
+ void setUp() {
+ Project project = new Project(PROJECT_UUID, "project-key", PROJECT_NAME, "project-description", emptyList());
+ when(analysisMetadataHolder.isBranch()).thenReturn(true);
+ when(analysisMetadataHolder.getProject()).thenReturn(project);
+ when(analysisMetadataHolder.getBranch()).thenReturn(new TestBranch(FEATURE_BRANCH));
+
+ when(periodHolder.hasPeriod()).thenReturn(true);
+ Period period = new Period(NewCodePeriodType.REFERENCE_BRANCH.name(), MAIN_BRANCH, null);
+ when(periodHolder.getPeriod()).thenReturn(period);
+ when(periodHolder.getPeriodOrigin()).thenReturn(PeriodOrigin.SCANNER);
+
+ when(dbClient.openSession(false)).thenReturn(dbSession);
+ when(dbClient.newCodePeriodDao()).thenReturn(newCodePeriodeDao);
+
+ Component root = mock(Component.class);
+ when(treeRootHolder.getRoot()).thenReturn(root);
+ when(root.getUuid()).thenReturn(BRANCH_UUID);
+
+ }
+
+ @Test
+ void getDescription() {
+ assertThat(persistReferenceBranchPeriodStep.getDescription()).isEqualTo("Persist or update reference branch new code period");
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenNotABranch() {
+ when(analysisMetadataHolder.isBranch()).thenReturn(false);
+ verifyExecuteNotCalled();
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenNoPeriods() {
+ when(periodHolder.hasPeriod()).thenReturn(false);
+ verifyExecuteNotCalled();
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenNotReferenceBranchPeriod() {
+ Period period = new Period("not-ref-branch", MAIN_BRANCH, null);
+ when(periodHolder.getPeriod()).thenReturn(period);
+ when(periodHolder.getPeriodOrigin()).thenReturn(PeriodOrigin.SETTINGS);
+ verifyExecuteNotCalled();
+ }
+
+ private void verifyExecuteNotCalled() {
+ PersistReferenceBranchPeriodStep spyStep = spy(persistReferenceBranchPeriodStep);
+
+ spyStep.execute(context);
+
+ verify(spyStep, never()).executePersistPeriodStep();
+ }
+
+ @Test
+ void execute_shouldCreateNewCodePeriod_whenItDoesNotExists() {
+ NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(MAIN_BRANCH);
+ when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.empty());
+
+ persistReferenceBranchPeriodStep.execute(context);
+
+ assertThat(logs.logs(DEBUG)).contains(
+ String.format("Persisting reference branch new code period '%s' for project '%s' and branch '%s'",MAIN_BRANCH, PROJECT_NAME, FEATURE_BRANCH));
+ ArgumentCaptor<NewCodePeriodDto> newCodePeriodCaptor = ArgumentCaptor.forClass(NewCodePeriodDto.class);
+ verify(newCodePeriodeDao).insert(eq(dbSession), newCodePeriodCaptor.capture());
+ assertThat(newCodePeriodCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedNewCodePeriod);
+ }
+
+ @Test
+ void execute_shouldUpdateNewCodePeriod_whenItExistsAndItChanged() {
+ NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(MAIN_BRANCH);
+ var newCodePeriodInBase = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue("old-value");
+
+ when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.of(newCodePeriodInBase));
+
+ persistReferenceBranchPeriodStep.execute(context);
+
+ assertThat(logs.logs(DEBUG)).contains(
+ String.format("Updating reference branch new code period '%s' for project '%s' and branch '%s'", MAIN_BRANCH ,PROJECT_NAME, FEATURE_BRANCH));
+ ArgumentCaptor<NewCodePeriodDto> newCodePeriodCaptor = ArgumentCaptor.forClass(NewCodePeriodDto.class);
+ verify(newCodePeriodeDao).update(eq(dbSession), newCodePeriodCaptor.capture());
+ assertThat(newCodePeriodCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedNewCodePeriod);
+ }
+
+ @Test
+ void execute_shouldDoNothing_whenItExistsAndItDidNotChanged() {
+ NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto()
+ .setBranchUuid(BRANCH_UUID)
+ .setProjectUuid(PROJECT_UUID)
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue(MAIN_BRANCH);
+
+ when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.of(expectedNewCodePeriod));
+
+ persistReferenceBranchPeriodStep.execute(context);
+
+ verify(newCodePeriodeDao, never()).update(any(), any());
+ verify(newCodePeriodeDao, never()).insert(any(), any());
+ }
+
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/testFixtures/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryRule.java b/server/sonar-ce-task-projectanalysis/src/testFixtures/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryRule.java
index 8a8d9ac2512..0b9ce68bc0f 100644
--- a/server/sonar-ce-task-projectanalysis/src/testFixtures/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryRule.java
+++ b/server/sonar-ce-task-projectanalysis/src/testFixtures/java/org/sonar/ce/task/projectanalysis/metric/MetricRepositoryRule.java
@@ -100,6 +100,11 @@ public class MetricRepositoryRule extends ExternalResource implements MetricRepo
}
@Override
+ public Optional<Metric> getOptionalByKey(String key) {
+ return Optional.ofNullable(metricsByKey.get(key));
+ }
+
+ @Override
public Metric getByUuid(String uuid) {
Metric res = metricsByUuid.get(uuid);
checkState(res != null, format("No Metric can be found for uuid %s", uuid));
diff --git a/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java b/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
index 3bfa5184256..40981cd7d13 100644
--- a/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
+++ b/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
@@ -108,6 +108,7 @@ public final class SqTables {
"rules_parameters",
"rules_profiles",
"rule_repositories",
+ "sca_analyses",
"sca_dependencies",
"sca_encountered_licenses",
"sca_issues",
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java
index 0ece24490e0..51585695dbc 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java
@@ -92,23 +92,16 @@ class ProjectAlmSettingDaoIT {
@Test
void select_by_alm_setting_and_slugs() {
- when(uuidFactory.create()).thenReturn(A_UUID);
AlmSettingDto almSettingsDto = db.almSettings().insertBitbucketAlmSetting();
- ProjectDto project = db.components().insertPrivateProject().getProjectDto();
- ProjectAlmSettingDto bitbucketProjectAlmSettingDto = newBitbucketProjectAlmSettingDto(almSettingsDto, project);
- bitbucketProjectAlmSettingDto.setAlmSlug("slug1");
- underTest.insertOrUpdate(dbSession, bitbucketProjectAlmSettingDto, almSettingsDto.getKey(), project.getName(), project.getKey());
- ProjectAlmSettingDto bitbucketProjectAlmSettingDto2 = newBitbucketProjectAlmSettingDto(almSettingsDto,
- db.components().insertPrivateProject().getProjectDto());
- bitbucketProjectAlmSettingDto2.setAlmSlug("slug2");
- when(uuidFactory.create()).thenReturn(A_UUID + 1);
- underTest.insertOrUpdate(dbSession, bitbucketProjectAlmSettingDto2, almSettingsDto.getKey(), project.getName(), project.getKey());
-
- Set<String> slugs = new HashSet<>();
- slugs.add("slug1");
- assertThat(underTest.selectByAlmSettingAndSlugs(dbSession, almSettingsDto, slugs))
+ ProjectAlmSettingDto matchingProject = createBitbucketProject(almSettingsDto, dto -> dto.setAlmSlug("slug1"));
+ createBitbucketProject(almSettingsDto, dto -> dto.setAlmSlug("slug2"));
+
+ Set<String> slugs = Set.of("slug1");
+ List<ProjectAlmSettingDto> results = underTest.selectByAlmSettingAndSlugs(dbSession, almSettingsDto, slugs);
+
+ assertThat(results)
.extracting(ProjectAlmSettingDto::getProjectUuid, ProjectAlmSettingDto::getSummaryCommentEnabled, ProjectAlmSettingDto::getInlineAnnotationsEnabled)
- .containsExactly(tuple(project.getUuid(), bitbucketProjectAlmSettingDto2.getSummaryCommentEnabled(), bitbucketProjectAlmSettingDto2.getInlineAnnotationsEnabled()));
+ .containsExactly(tuple(matchingProject.getProjectUuid(), matchingProject.getSummaryCommentEnabled(), matchingProject.getInlineAnnotationsEnabled()));
}
@Test
@@ -142,12 +135,7 @@ class ProjectAlmSettingDaoIT {
}
private ProjectAlmSettingDto createAlmProject(AlmSettingDto almSettingsDto, Consumer<ProjectAlmSettingDto>... populators) {
- ProjectDto project = db.components().insertPrivateProject().getProjectDto();
- when(uuidFactory.create()).thenReturn(project.getUuid() + "_set");
- ProjectAlmSettingDto projectAlmSettingDto = newGithubProjectAlmSettingDto(almSettingsDto, project);
- stream(populators).forEach(p -> p.accept(projectAlmSettingDto));
- underTest.insertOrUpdate(dbSession, projectAlmSettingDto, almSettingsDto.getKey(), project.getName(), project.getKey());
- return projectAlmSettingDto;
+ return createAlmProject(almSettingsDto, dto -> stream(populators).forEach(p -> p.accept(dto)), project -> newGithubProjectAlmSettingDto(almSettingsDto, project));
}
@Test
@@ -160,23 +148,16 @@ class ProjectAlmSettingDaoIT {
@Test
void select_by_alm_setting_and_repos() {
- when(uuidFactory.create()).thenReturn(A_UUID);
AlmSettingDto almSettingsDto = db.almSettings().insertGitHubAlmSetting();
- ProjectDto project = db.components().insertPrivateProject().getProjectDto();
- ProjectAlmSettingDto githubProjectAlmSettingDto = newGithubProjectAlmSettingDto(almSettingsDto, project);
- githubProjectAlmSettingDto.setAlmRepo("repo1");
- underTest.insertOrUpdate(dbSession, githubProjectAlmSettingDto, almSettingsDto.getKey(), project.getName(), project.getKey());
- ProjectAlmSettingDto githubProjectAlmSettingDto2 = newGithubProjectAlmSettingDto(almSettingsDto,
- db.components().insertPrivateProject().getProjectDto());
- githubProjectAlmSettingDto2.setAlmRepo("repo2");
- when(uuidFactory.create()).thenReturn(A_UUID + 1);
- underTest.insertOrUpdate(dbSession, githubProjectAlmSettingDto2, almSettingsDto.getKey(), project.getName(), project.getKey());
-
- Set<String> repos = new HashSet<>();
- repos.add("repo1");
- assertThat(underTest.selectByAlmSettingAndRepos(dbSession, almSettingsDto, repos))
+ ProjectAlmSettingDto matchingProject = createAlmProject(almSettingsDto, dto -> dto.setAlmRepo("repo1"));
+ createAlmProject(almSettingsDto, dto -> dto.setAlmRepo("repo2"));
+
+ Set<String> repos = Set.of("repo1");
+ List<ProjectAlmSettingDto> results = underTest.selectByAlmSettingAndRepos(dbSession, almSettingsDto, repos);
+
+ assertThat(results)
.extracting(ProjectAlmSettingDto::getProjectUuid, ProjectAlmSettingDto::getSummaryCommentEnabled)
- .containsExactly(tuple(project.getUuid(), githubProjectAlmSettingDto.getSummaryCommentEnabled()));
+ .containsExactly(tuple(matchingProject.getProjectUuid(), matchingProject.getSummaryCommentEnabled()));
}
@Test
@@ -208,8 +189,8 @@ class ProjectAlmSettingDaoIT {
ProjectAlmKeyAndProject::getProjectUuid,
ProjectAlmKeyAndProject::getAlmId,
ProjectAlmKeyAndProject::getUrl,
- ProjectAlmKeyAndProject::getMonorepo
- ).containsExactlyInAnyOrder(
+ ProjectAlmKeyAndProject::getMonorepo)
+ .containsExactlyInAnyOrder(
tuple(project1.getUuid(), almSettingsDto.getAlm().getId(), almSettingsDto.getUrl(), false),
tuple(project2.getUuid(), almSettingsDto.getAlm().getId(), almSettingsDto.getUrl(), true));
}
@@ -243,18 +224,20 @@ class ProjectAlmSettingDaoIT {
AlmSettingDto matchingAlmSettingDto = db.almSettings().insertGitHubAlmSetting();
AlmSettingDto notMatchingAlmSettingDto = db.almSettings().insertGitHubAlmSetting();
ProjectAlmSettingDto matchingRepo = createAlmProject(matchingAlmSettingDto, dto -> dto.setAlmRepo("matchingRepo"));
- ProjectAlmSettingDto notMatchingRepo = createAlmProject(matchingAlmSettingDto, dto -> dto.setAlmRepo("whatever"));
ProjectAlmSettingDto matchingAlmSetting = createAlmProject(matchingAlmSettingDto, dto -> dto.setAlmRepo("matchingRepo"));
- ProjectAlmSettingDto notMatchingAlmSetting = createAlmProject(notMatchingAlmSettingDto, dto -> dto.setAlmRepo("matchingRepo"));
+ createAlmProject(matchingAlmSettingDto, dto -> dto.setAlmRepo("whatever")); // not matching repo
+ createAlmProject(notMatchingAlmSettingDto, dto -> dto.setAlmRepo("matchingRepo")); // not matching alm setting
- List<ProjectAlmSettingDto> dtos = underTest.selectProjectAlmSettings(dbSession, new ProjectAlmSettingQuery("matchingRepo", matchingAlmSettingDto.getUuid()), 1, 100);
- assertThat(dtos)
+ ProjectAlmSettingQuery query = new ProjectAlmSettingQuery("matchingRepo", matchingAlmSettingDto.getUuid());
+ List<ProjectAlmSettingDto> results = underTest.selectProjectAlmSettings(dbSession, query, 1, 100);
+
+ assertThat(results)
.usingRecursiveFieldByFieldElementComparator()
.containsExactlyInAnyOrder(matchingRepo, matchingAlmSetting);
}
private static Object[][] paginationTestCases() {
- return new Object[][]{
+ return new Object[][] {
{100, 1, 5},
{100, 3, 18},
{2075, 41, 50},
@@ -265,7 +248,7 @@ class ProjectAlmSettingDaoIT {
@ParameterizedTest
@MethodSource("paginationTestCases")
void selectProjectAlmSettings_whenUsingPagination_findsTheRightResults(int numberToGenerate, int offset, int limit) {
- when(uuidFactory.create()).thenAnswer(answer -> UUID.randomUUID().toString());
+ when(uuidFactory.create()).thenAnswer(answer -> UUID.randomUUID().toString());
Map<String, ProjectAlmSettingDto> allProjectAlmSettingsDtos = generateProjectAlmSettingsDtos(numberToGenerate);
@@ -284,10 +267,10 @@ class ProjectAlmSettingDaoIT {
}
Map<String, ProjectAlmSettingDto> result = IntStream.range(1000, 1000 + numberToGenerate)
.mapToObj(i -> underTest.insertOrUpdate(dbSession, new ProjectAlmSettingDto()
- .setAlmRepo("repo_" + i)
- .setAlmSettingUuid("almSettingUuid_" + i)
- .setProjectUuid("projectUuid_" + i)
- .setMonorepo(false),
+ .setAlmRepo("repo_" + i)
+ .setAlmSettingUuid("almSettingUuid_" + i)
+ .setProjectUuid("projectUuid_" + i)
+ .setMonorepo(false),
"key_" + i, "projectName_" + i, "projectKey_" + i))
.collect(toMap(ProjectAlmSettingDto::getAlmRepo, Function.identity()));
db.commit();
@@ -384,4 +367,83 @@ class ProjectAlmSettingDaoIT {
assertThat(underTest.countByAlmSetting(dbSession, githubAlmSetting1)).isOne();
}
+ @Test
+ void selectProjectAlmSettings_whenSearchingByAlmRepo_returnsMatchingResults() {
+ AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting();
+ ProjectAlmSettingDto matchingProject = createAlmProject(almSetting, dto -> dto.setAlmRepo("target-repo"));
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("other-repo"));
+ createAlmProject(almSetting, dto -> dto.setAlmSlug("target-repo")); // slug should not match
+
+ ProjectAlmSettingQuery query = ProjectAlmSettingQuery.forAlmRepo("target-repo");
+ List<ProjectAlmSettingDto> results = underTest.selectProjectAlmSettings(dbSession, query, 1, 100);
+
+ assertThat(results)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactly(matchingProject);
+ }
+
+ @Test
+ void selectProjectAlmSettings_whenSearchingByAlmRepoAndSlug_returnsMatchingResults() {
+ AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting();
+ ProjectAlmSettingDto matchingProject = createAzureProject(almSetting, "target-repo", "target-project");
+ createAzureProject(almSetting, "target-repo", "other-project"); // different slug
+ createAzureProject(almSetting, "other-repo", "target-project"); // different repo
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("target-repo")); // missing slug
+
+ ProjectAlmSettingQuery query = ProjectAlmSettingQuery.forAlmRepoAndSlug("target-repo", "target-project");
+ List<ProjectAlmSettingDto> results = underTest.selectProjectAlmSettings(dbSession, query, 1, 100);
+
+ assertThat(results)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactly(matchingProject);
+ }
+
+ @Test
+ void selectProjectAlmSettings_whenSearchingByCaseInsensitiveAlmRepo_returnsMatchingResults() {
+ AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting();
+ ProjectAlmSettingDto upperCaseProject = createAlmProject(almSetting, dto -> dto.setAlmRepo("TARGET-REPO"));
+ ProjectAlmSettingDto lowerCaseProject = createAlmProject(almSetting, dto -> dto.setAlmRepo("target-repo"));
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("other-repo"));
+
+ ProjectAlmSettingQuery query = ProjectAlmSettingQuery.forAlmRepo("Target-Repo");
+ List<ProjectAlmSettingDto> results = underTest.selectProjectAlmSettings(dbSession, query, 1, 100);
+
+ assertThat(results)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactlyInAnyOrder(upperCaseProject, lowerCaseProject);
+ }
+
+ @Test
+ void countProjectAlmSettings_whenSearchingByAlmRepo_returnsCorrectCount() {
+ AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting();
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("target-repo"));
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("target-repo"));
+ createAlmProject(almSetting, dto -> dto.setAlmRepo("other-repo"));
+
+ ProjectAlmSettingQuery query = ProjectAlmSettingQuery.forAlmRepo("target-repo");
+ int count = underTest.countProjectAlmSettings(dbSession, query);
+
+ assertThat(count).isEqualTo(2);
+ }
+
+ private ProjectAlmSettingDto createAzureProject(AlmSettingDto almSettingsDto, String almRepo, String almSlug) {
+ return createAlmProject(almSettingsDto, dto -> {
+ dto.setAlmRepo(almRepo);
+ dto.setAlmSlug(almSlug);
+ }, project -> newAzureProjectAlmSettingDto(almSettingsDto, project));
+ }
+
+ private ProjectAlmSettingDto createBitbucketProject(AlmSettingDto almSettingsDto, Consumer<ProjectAlmSettingDto> customizer) {
+ return createAlmProject(almSettingsDto, customizer, project -> newBitbucketProjectAlmSettingDto(almSettingsDto, project));
+ }
+
+ private ProjectAlmSettingDto createAlmProject(AlmSettingDto almSettingsDto, Consumer<ProjectAlmSettingDto> customizer, Function<ProjectDto, ProjectAlmSettingDto> dtoFactory) {
+ ProjectDto project = db.components().insertPrivateProject().getProjectDto();
+ when(uuidFactory.create()).thenReturn(project.getUuid() + "_set");
+ ProjectAlmSettingDto projectAlmSettingDto = dtoFactory.apply(project);
+ customizer.accept(projectAlmSettingDto);
+ underTest.insertOrUpdate(dbSession, projectAlmSettingDto, almSettingsDto.getKey(), project.getName(), project.getKey());
+ return projectAlmSettingDto;
+ }
+
}
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java
index 2b1cb099fca..feae0d3f20c 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java
@@ -906,6 +906,41 @@ class BranchDaoIT {
tuple(branch1.getUuid(), projectData1.projectUuid(), true));
}
+ @Test
+ void selectPullRequestsTargetingBranch() {
+ BranchDto mainBranch = new BranchDto();
+ mainBranch.setProjectUuid("U1");
+ mainBranch.setUuid("U1");
+ mainBranch.setIsMain(true);
+ mainBranch.setBranchType(BranchType.BRANCH);
+ mainBranch.setKey("master");
+ underTest.insert(dbSession, mainBranch);
+
+ BranchDto prBranch = new BranchDto();
+ prBranch.setProjectUuid("U1");
+ prBranch.setUuid("U2");
+ prBranch.setIsMain(false);
+ prBranch.setBranchType(PULL_REQUEST);
+ prBranch.setKey("1234");
+ prBranch.setMergeBranchUuid("U1");
+ underTest.insert(dbSession, prBranch);
+
+ // make a second PR also targeting main branch
+ prBranch.setUuid("U3");
+ prBranch.setKey("4321");
+ prBranch.setMergeBranchUuid("U1");
+ underTest.insert(dbSession, prBranch);
+
+ // make a third PR NOT targeting main branch to be sure we filter it out
+ prBranch.setUuid("U4");
+ prBranch.setKey("5678");
+ prBranch.setMergeBranchUuid("U42");
+ underTest.insert(dbSession, prBranch);
+
+ var result = underTest.selectPullRequestsTargetingBranch(dbSession, "U1", "U1");
+ assertThat(result.stream().map(BranchDto::getUuid).toList()).containsExactlyInAnyOrder("U2", "U3");
+ }
+
private void insertBranchesForProjectUuids(boolean mainBranch, String... uuids) {
for (String uuid : uuids) {
BranchDto dto = new BranchDto();
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java
index e1fb868b7c0..f468306e8f8 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java
@@ -1994,13 +1994,37 @@ oldCreationDate));
// the issue uuids here don't even exist but doesn't matter, we don't delete issues so not testing that
var issueReleaseBase = Map.of("created_at", 0L, "updated_at", 0L,
- "severity", "INFO", "severity_sort_key", 42, "status", "TO_REVIEW");
+ "severity", "INFO", "original_severity", "INFO", "manual_severity", false,
+ "severity_sort_key", 42, "status", "TO_REVIEW");
db.executeInsert("sca_issues_releases", merge(issueReleaseBase, Map.of("uuid", "issue-release-uuid1",
"sca_issue_uuid", "issue-uuid1", "sca_release_uuid", "release-uuid1")));
db.executeInsert("sca_issues_releases", merge(issueReleaseBase, Map.of("uuid", "issue-release-uuid2",
"sca_issue_uuid", "issue-uuid2", "sca_release_uuid", "release-uuid2")));
assertThat(db.countRowsOfTable(dbSession, "sca_issues_releases")).isEqualTo(2);
+
+ var issueReleaseChangeBase = Map.of("created_at", 0L, "updated_at", 0L);
+ db.executeInsert("sca_issue_rels_changes", merge(issueReleaseChangeBase, Map.of("uuid", "issue-release-change-uuid1",
+ "sca_issues_releases_uuid", "issue-release-uuid1")));
+ db.executeInsert("sca_issue_rels_changes", merge(issueReleaseChangeBase, Map.of("uuid", "issue-release-change-uuid2",
+ "sca_issues_releases_uuid", "issue-release-uuid2")));
+
+ assertThat(db.countRowsOfTable(dbSession, "sca_issue_rels_changes")).isEqualTo(2);
+
+ var analysisBase = Map.of(
+ "created_at", 0L,
+ "updated_at", 0L,
+ "status", "COMPLETED",
+ "errors", "[]",
+ "parsed_files", "[]",
+ "failed_reason", "something");
+ db.executeInsert("sca_analyses", merge(analysisBase, Map.of(
+ "uuid", "analysis-uuid1",
+ "component_uuid", branch1Uuid)));
+ db.executeInsert("sca_analyses", merge(analysisBase, Map.of(
+ "uuid", "analysis-uuid2",
+ "component_uuid", branch2Uuid)));
+ assertThat(db.countRowsOfTable(dbSession, "sca_analyses")).isEqualTo(2);
}
@Test
@@ -2016,6 +2040,49 @@ oldCreationDate));
assertThat(db.countRowsOfTable(dbSession, "sca_releases")).isEqualTo(1);
assertThat(db.countRowsOfTable(dbSession, "sca_dependencies")).isEqualTo(1);
assertThat(db.countRowsOfTable(dbSession, "sca_issues_releases")).isEqualTo(1);
+ assertThat(db.countRowsOfTable(dbSession, "sca_issue_rels_changes")).isEqualTo(1);
+ assertThat(db.countRowsOfTable(dbSession, "sca_analyses")).isEqualTo(1);
+ }
+
+ @Test
+ void deleteProject_purgesScaLicenseProfiles() {
+ ProjectDto project = db.components().insertPublicProject().getProjectDto();
+
+ var scaLicenseProfileProjectBase = Map.of(
+ "sca_license_profile_uuid", "sca-license-profile-uuid1",
+ "created_at", 0L,
+ "updated_at", 0L);
+
+ db.executeInsert("sca_lic_prof_projects", merge(scaLicenseProfileProjectBase, Map.of(
+ "uuid", "sca-lic-prof-project-uuid1",
+ "project_uuid", project.getUuid())));
+
+ db.executeInsert("sca_lic_prof_projects", merge(scaLicenseProfileProjectBase, Map.of(
+ "uuid", "sca-lic-prof-project-uuid2",
+ "project_uuid", "other-project-uuid")));
+
+ assertThat(db.countRowsOfTable(dbSession, "sca_lic_prof_projects")).isEqualTo(2);
+
+ underTest.deleteProject(dbSession, project.getUuid(), project.getQualifier(), project.getName(), project.getKey());
+
+ assertThat(db.countRowsOfTable(dbSession, "sca_lic_prof_projects")).isEqualTo(1);
+ }
+
+ @Test
+ void whenDeleteBranch_thenPurgeArchitectureGraphs() {
+ ProjectDto project = db.components().insertPublicProject().getProjectDto();
+ BranchDto branch1 = db.components().insertProjectBranch(project);
+ BranchDto branch2 = db.components().insertProjectBranch(project);
+
+ db.executeInsert("architecture_graphs", Map.of("uuid", "12345", "branch_uuid", branch1.getUuid(), "ecosystem", "xoo", "type", "file_graph", "graph_data", "{}"));
+ db.executeInsert("architecture_graphs", Map.of("uuid", "123456", "branch_uuid", branch1.getUuid(), "ecosystem", "xoo", "type", "class_graph", "graph_data", "{}"));
+ db.executeInsert("architecture_graphs", Map.of("uuid", "1234567", "branch_uuid", branch2.getUuid(), "ecosystem", "xoo", "type", "file_graph", "graph_data", "{}"));
+
+ assertThat(db.countRowsOfTable(dbSession, "architecture_graphs")).isEqualTo(3);
+ underTest.deleteBranch(dbSession, branch1.getUuid());
+ assertThat(db.countRowsOfTable(dbSession, "architecture_graphs")).isEqualTo(1);
+ underTest.deleteBranch(dbSession, branch2.getUuid());
+ assertThat(db.countRowsOfTable(dbSession, "architecture_graphs")).isZero();
}
private AnticipatedTransitionDto getAnticipatedTransitionsDto(String uuid, String projectUuid, Date creationDate) {
diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java
index 9b52c0d7264..16e86bcd2e5 100644
--- a/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java
+++ b/server/sonar-db-dao/src/it/java/org/sonar/db/user/GroupDaoIT.java
@@ -223,6 +223,47 @@ class GroupDaoIT {
}
@Test
+ void findByQuery_withUserId_countAndFindExpectedUsers() {
+ GroupDto group1 = db.users().insertGroup("sonar-users");
+ GroupDto group2 = db.users().insertGroup("SONAR-ADMINS");
+ GroupDto group3 = db.users().insertGroup("customers-group1");
+
+ UserDto user = db.users().insertUser();
+ UserDto user2 = db.users().insertUser();
+ db.users().insertMember(group1, user);
+ db.users().insertMember(group2, user);
+ db.users().insertMember(group2, user2);
+ db.users().insertMember(group3, user2);
+
+ GroupQuery groupQueryIncludingUser = GroupQuery.builder().userId(user.getUuid()).build();
+
+ assertThat(underTest.countByQuery(dbSession, groupQueryIncludingUser)).isEqualTo(2);
+ assertThat(underTest.selectByQuery(dbSession, groupQueryIncludingUser, 1, 100))
+ .extracting(GroupDto::getUuid)
+ .containsExactlyInAnyOrder(group1.getUuid(), group2.getUuid());
+ }
+
+ @Test
+ void findByQuery_withExcludedUserId_countAndFindExpectedUsers() {
+ GroupDto group1 = db.users().insertGroup("sonar-users");
+ GroupDto group2 = db.users().insertGroup("SONAR-ADMINS");
+ GroupDto group3 = db.users().insertGroup("customers-group1");
+
+ UserDto user = db.users().insertUser();
+ UserDto user2 = db.users().insertUser();
+ db.users().insertMember(group1, user);
+ db.users().insertMember(group2, user);
+ db.users().insertMember(group2, user2);
+ db.users().insertMember(group3, user2);
+
+ GroupQuery groupQueryExcludingUser = GroupQuery.builder().excludedUserId(user.getUuid()).build();
+ assertThat(underTest.countByQuery(dbSession, groupQueryExcludingUser)).isEqualTo(1);
+ assertThat(underTest.selectByQuery(dbSession, groupQueryExcludingUser, 1, 100))
+ .extracting(GroupDto::getUuid)
+ .containsExactlyInAnyOrder(group3.getUuid());
+ }
+
+ @Test
void deleteByUuid() {
db.getDbClient().groupDao().insert(dbSession, aGroup);
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDto.java
index 95f83df37d7..2d0e5766eb9 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDto.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDto.java
@@ -19,6 +19,7 @@
*/
package org.sonar.db.alm.setting;
+import java.util.Objects;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
@@ -165,5 +166,21 @@ public class ProjectAlmSettingDto {
this.createdAt = createdAt;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ProjectAlmSettingDto that = (ProjectAlmSettingDto) o;
+ return Objects.equals(uuid, that.uuid);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uuid);
+ }
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java
index ea3d8fd1b47..54c7f6621f5 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java
@@ -21,6 +21,73 @@ package org.sonar.db.alm.setting;
import javax.annotation.Nullable;
-public record ProjectAlmSettingQuery(@Nullable String repository, @Nullable String almSettingUuid
-) {
+public record ProjectAlmSettingQuery(
+ @Nullable String repository,
+ @Nullable String almSettingUuid,
+ @Nullable String almRepo,
+ @Nullable String almSlug) {
+
+ // Existing constructor for backward compatibility (repository search in both alm_repo and alm_slug)
+ public ProjectAlmSettingQuery(String repository, String almSettingUuid) {
+ this(repository, almSettingUuid, null, null);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String repository;
+ private String almSettingUuid;
+ private String almRepo;
+ private String almSlug;
+
+ private Builder() {
+ }
+
+ public Builder repository(String repository) {
+ if (almRepo != null || almSlug != null) {
+ throw new IllegalStateException("Cannot use repository with almRepo or almSlug");
+ }
+ this.repository = repository;
+ return this;
+ }
+
+ public Builder almSettingUuid(String almSettingUuid) {
+ if (almRepo != null || almSlug != null) {
+ throw new IllegalStateException("Cannot use almSettingUuid with almRepo or almSlug");
+ }
+ this.almSettingUuid = almSettingUuid;
+ return this;
+ }
+
+ public Builder almRepo(String almRepo) {
+ if (repository != null || almSettingUuid != null) {
+ throw new IllegalStateException("Cannot use almRepo with repository or almSettingUuid");
+ }
+ this.almRepo = almRepo;
+ return this;
+ }
+
+ public Builder almSlug(String almSlug) {
+ if (repository != null || almSettingUuid != null) {
+ throw new IllegalStateException("Cannot use almSlug with repository or almSettingUuid");
+ }
+ this.almSlug = almSlug;
+ return this;
+ }
+
+ public ProjectAlmSettingQuery build() {
+ return new ProjectAlmSettingQuery(repository, almSettingUuid, almRepo, almSlug);
+ }
+ }
+
+ public static ProjectAlmSettingQuery forAlmRepo(String almRepo) {
+ return builder().almRepo(almRepo).build();
+ }
+
+ public static ProjectAlmSettingQuery forAlmRepoAndSlug(String almRepo, String almSlug) {
+ return builder().almRepo(almRepo).almSlug(almSlug).build();
+ }
+
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java
index 1f72e4a4002..faa712d555d 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java
@@ -214,4 +214,8 @@ public class BranchDao implements Dao {
public List<BranchDto> selectMainBranchesAssociatedToDefaultQualityProfile(DbSession dbSession) {
return mapper(dbSession).selectMainBranchesAssociatedToDefaultQualityProfile();
}
+
+ public List<BranchDto> selectPullRequestsTargetingBranch(DbSession dbSession, String projectUuid, String branchUuid) {
+ return mapper(dbSession).selectPullRequestsTargetingBranch(projectUuid, branchUuid);
+ }
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java
index 04c4642834f..dbd11313580 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java
@@ -81,4 +81,6 @@ public interface BranchMapper {
List<BranchDto> selectMainBranches();
List<BranchDto> selectMainBranchesAssociatedToDefaultQualityProfile();
+
+ List<BranchDto> selectPullRequestsTargetingBranch(@Param("projectUuid") String projectUuid, @Param("branchUuid") String branchUuid);
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java
index 230d9aff010..fca991f9f28 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java
@@ -510,6 +510,13 @@ class PurgeCommands {
profiler.stop();
}
+ public void deleteArchitectureGraphs(String branchUuid) {
+ profiler.start("deleteArchitectureGraphs (architecture_graphs)");
+ purgeMapper.deleteArchitectureGraphsByBranchUuid(branchUuid);
+ session.commit();
+ profiler.stop();
+ }
+
public void deleteAnticipatedTransitions(String projectUuid, long createdAt) {
profiler.start("deleteAnticipatedTransitions (anticipated_transitions)");
purgeMapper.deleteAnticipatedTransitionsByProjectUuidAndCreationDate(projectUuid, createdAt);
@@ -525,11 +532,24 @@ class PurgeCommands {
}
public void deleteScaActivity(String componentUuid) {
+ // delete sca_analyses first since it sort of marks the analysis as valid/existing
+ profiler.start("deleteScaAnalyses (sca_analyses)");
+ purgeMapper.deleteScaAnalysesByComponentUuid(componentUuid);
+ session.commit();
+ profiler.stop();
+
profiler.start("deleteScaDependencies (sca_dependencies)");
purgeMapper.deleteScaDependenciesByComponentUuid(componentUuid);
session.commit();
profiler.stop();
+ // this must be done before deleting sca_issues_releases or we won't
+ // be able to find the rows
+ profiler.start("deleteScaIssuesReleasesChanges (sca_issue_rels_changes)");
+ purgeMapper.deleteScaIssuesReleasesChangesByComponentUuid(componentUuid);
+ session.commit();
+ profiler.stop();
+
profiler.start("deleteScaIssuesReleases (sca_issues_releases)");
purgeMapper.deleteScaIssuesReleasesByComponentUuid(componentUuid);
session.commit();
@@ -542,4 +562,10 @@ class PurgeCommands {
session.commit();
profiler.stop();
}
+
+ public void deleteScaLicenseProfiles(String projectUuid) {
+ profiler.start("deleteScaLicenseProfileProjects (sca_lic_prof_projects)");
+ purgeMapper.deleteScaLicenseProfileProjectsByProjectUuid(projectUuid);
+ profiler.stop();
+ }
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java
index ce5e0cf5e70..ff34ce5783d 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java
@@ -281,6 +281,7 @@ public class PurgeDao implements Dao {
commands.deleteReportSubscriptions(branchUuid);
commands.deleteIssuesFixed(branchUuid);
commands.deleteScaActivity(branchUuid);
+ commands.deleteArchitectureGraphs(branchUuid);
}
private static void deleteProject(String projectUuid, PurgeMapper mapper, PurgeCommands commands) {
@@ -313,6 +314,7 @@ public class PurgeDao implements Dao {
commands.deleteOutdatedProperties(projectUuid);
commands.deleteReportSchedules(projectUuid);
commands.deleteReportSubscriptions(projectUuid);
+ commands.deleteScaLicenseProfiles(projectUuid);
}
/**
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java
index 5ca08a12d7a..ab4b369aef6 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java
@@ -195,9 +195,17 @@ public interface PurgeMapper {
void deleteIssuesFixedByBranchUuid(@Param("branchUuid") String branchUuid);
+ void deleteScaAnalysesByComponentUuid(@Param("componentUuid") String componentUuid);
+
void deleteScaDependenciesByComponentUuid(@Param("componentUuid") String componentUuid);
void deleteScaIssuesReleasesByComponentUuid(@Param("componentUuid") String componentUuid);
+ void deleteScaIssuesReleasesChangesByComponentUuid(@Param("componentUuid") String componentUuid);
+
void deleteScaReleasesByComponentUuid(@Param("componentUuid") String componentUuid);
+
+ void deleteScaLicenseProfileProjectsByProjectUuid(@Param("projectUuid") String projectUuid);
+
+ void deleteArchitectureGraphsByBranchUuid(@Param("branchUuid") String branchUuid);
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java
index a2185648d6b..b54c25da22d 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupQuery.java
@@ -29,10 +29,14 @@ import org.sonar.db.WildcardPosition;
public class GroupQuery {
private final String searchText;
private final String isManagedSqlClause;
+ private final String userId;
+ private final String excludedUserId;
- GroupQuery(@Nullable String searchText, @Nullable String isManagedSqlClause) {
+ GroupQuery(@Nullable String searchText, @Nullable String isManagedSqlClause, String userId, String excludedUserId) {
this.searchText = searchTextToSearchTextSql(searchText);
this.isManagedSqlClause = isManagedSqlClause;
+ this.userId = userId;
+ this.excludedUserId = excludedUserId;
}
private static String searchTextToSearchTextSql(@Nullable String text) {
@@ -54,6 +58,16 @@ public class GroupQuery {
return isManagedSqlClause;
}
+ @CheckForNull
+ public String getUserId() {
+ return userId;
+ }
+
+ @CheckForNull
+ public String getExcludedUserId() {
+ return excludedUserId;
+ }
+
public static GroupQueryBuilder builder() {
return new GroupQueryBuilder();
}
@@ -61,6 +75,8 @@ public class GroupQuery {
public static final class GroupQueryBuilder {
private String searchText = null;
private String isManagedSqlClause = null;
+ private String userId = null;
+ private String excludedUserId = null;
private GroupQueryBuilder() {
}
@@ -70,14 +86,23 @@ public class GroupQuery {
return this;
}
-
public GroupQuery.GroupQueryBuilder isManagedClause(@Nullable String isManagedSqlClause) {
this.isManagedSqlClause = isManagedSqlClause;
return this;
}
+ public GroupQuery.GroupQueryBuilder userId(@Nullable String userId) {
+ this.userId = userId;
+ return this;
+ }
+
+ public GroupQuery.GroupQueryBuilder excludedUserId(@Nullable String excludedUserId) {
+ this.excludedUserId = excludedUserId;
+ return this;
+ }
+
public GroupQuery build() {
- return new GroupQuery(searchText, isManagedSqlClause);
+ return new GroupQuery(searchText, isManagedSqlClause, userId, excludedUserId);
}
}
}
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml
index 9f015b2d22e..e5de9d5a3e1 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml
@@ -215,6 +215,12 @@
<if test="query.almSettingUuid != null">
AND p.alm_setting_uuid = #{query.almSettingUuid, jdbcType=VARCHAR}
</if>
+ <if test="query.almRepo != null">
+ AND lower(p.alm_repo) = lower(#{query.almRepo, jdbcType=VARCHAR})
+ </if>
+ <if test="query.almSlug != null">
+ AND lower(p.alm_slug) = lower(#{query.almSlug, jdbcType=VARCHAR})
+ </if>
</where>
</sql>
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml
index f96a116d7c1..acf970ba7ac 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml
@@ -322,4 +322,13 @@
and p.uuid not in (select project_uuid from project_qprofiles)
</select>
+ <select id="selectPullRequestsTargetingBranch" resultType="org.sonar.db.component.BranchDto">
+ select <include refid="columns"/>
+ from project_branches pb
+ where
+ pb.project_uuid = #{projectUuid, jdbcType=VARCHAR}
+ and pb.merge_branch_uuid = #{branchUuid, jdbcType=VARCHAR}
+ and pb.branch_type = 'PULL_REQUEST'
+ </select>
+
</mapper>
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml
index 4a64f3cdeab..bc5c066d6b4 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml
@@ -670,14 +670,28 @@
delete from issues_fixed where pull_request_uuid = #{branchUuid,jdbcType=VARCHAR}
</delete>
+ <delete id="deleteArchitectureGraphsByBranchUuid">
+ delete from architecture_graphs where branch_uuid = #{branchUuid,jdbcType=VARCHAR}
+ </delete>
+ <delete id="deleteScaAnalysesByComponentUuid">
+ delete from sca_analyses where component_uuid = #{componentUuid,jdbcType=VARCHAR}
+ </delete>
<delete id="deleteScaDependenciesByComponentUuid">
delete from sca_dependencies where sca_release_uuid in (select uuid from sca_releases where component_uuid = #{componentUuid,jdbcType=VARCHAR})
</delete>
<delete id="deleteScaIssuesReleasesByComponentUuid">
delete from sca_issues_releases where sca_release_uuid in (select uuid from sca_releases where component_uuid = #{componentUuid,jdbcType=VARCHAR})
</delete>
+ <delete id="deleteScaIssuesReleasesChangesByComponentUuid">
+ delete from sca_issue_rels_changes where sca_issues_releases_uuid in
+ (select sca_issues_releases.uuid from sca_issues_releases join sca_releases on sca_releases.uuid = sca_issues_releases.sca_release_uuid
+ where sca_releases.component_uuid = #{componentUuid,jdbcType=VARCHAR})
+ </delete>
<delete id="deleteScaReleasesByComponentUuid">
delete from sca_releases where component_uuid = #{componentUuid,jdbcType=VARCHAR}
</delete>
+ <delete id="deleteScaLicenseProfileProjectsByProjectUuid">
+ delete from sca_lic_prof_projects where project_uuid = #{projectUuid,jdbcType=VARCHAR}
+ </delete>
</mapper>
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml
index 7adc671389e..87ed608b98a 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMapper.xml
@@ -113,6 +113,12 @@
<if test="query.isManagedSqlClause != null">
AND ${query.isManagedSqlClause}
</if>
+ <if test="query.userId != null">
+ AND g.uuid in (select group_uuid from groups_users gu where gu.user_uuid = #{query.userId,jdbcType=VARCHAR})
+ </if>
+ <if test="query.excludedUserId != null">
+ AND g.uuid not in (select group_uuid from groups_users gu where gu.user_uuid = #{query.excludedUserId,jdbcType=VARCHAR})
+ </if>
</where>
</sql>
</mapper>
diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl
index bb7e4166b03..35e16dff62b 100644
--- a/server/sonar-db-dao/src/schema/schema-sq.ddl
+++ b/server/sonar-db-dao/src/schema/schema-sq.ddl
@@ -121,12 +121,15 @@ CREATE INDEX "IDX_APP_PROJ_PROJECT_UUID" ON "APP_PROJECTS"("PROJECT_UUID" NULLS
CREATE TABLE "ARCHITECTURE_GRAPHS"(
"UUID" CHARACTER VARYING(40) NOT NULL,
"BRANCH_UUID" CHARACTER VARYING(40) NOT NULL,
- "SOURCE" CHARACTER VARYING(255) NOT NULL,
+ "ECOSYSTEM" CHARACTER VARYING(255) NOT NULL,
"TYPE" CHARACTER VARYING(255) NOT NULL,
- "GRAPH_DATA" CHARACTER LARGE OBJECT NOT NULL
+ "GRAPH_DATA" CHARACTER LARGE OBJECT NOT NULL,
+ "ANALYSIS_UUID" CHARACTER VARYING(40),
+ "PERSPECTIVE_KEY" CHARACTER VARYING(255),
+ "GRAPH_VERSION" CHARACTER VARYING(255) DEFAULT '1.0.0' NOT NULL
);
ALTER TABLE "ARCHITECTURE_GRAPHS" ADD CONSTRAINT "PK_ARCHITECTURE_GRAPHS" PRIMARY KEY("UUID");
-CREATE UNIQUE NULLS NOT DISTINCT INDEX "UQ_IDX_AG_BRANCH_TYPE_SOURCE" ON "ARCHITECTURE_GRAPHS"("BRANCH_UUID" NULLS FIRST, "TYPE" NULLS FIRST, "SOURCE" NULLS FIRST);
+CREATE UNIQUE NULLS NOT DISTINCT INDEX "UQ_IDX_AG_BRCH_TP_SRC_PSPCTV" ON "ARCHITECTURE_GRAPHS"("BRANCH_UUID" NULLS FIRST, "TYPE" NULLS FIRST, "ECOSYSTEM" NULLS FIRST, "PERSPECTIVE_KEY" NULLS FIRST);
CREATE TABLE "AUDITS"(
"UUID" CHARACTER VARYING(40) NOT NULL,
@@ -699,6 +702,7 @@ ALTER TABLE "PROJECT_ALM_SETTINGS" ADD CONSTRAINT "PK_PROJECT_ALM_SETTINGS" PRIM
CREATE UNIQUE NULLS NOT DISTINCT INDEX "UNIQ_PROJECT_ALM_SETTINGS" ON "PROJECT_ALM_SETTINGS"("PROJECT_UUID" NULLS FIRST);
CREATE INDEX "PROJECT_ALM_SETTINGS_ALM" ON "PROJECT_ALM_SETTINGS"("ALM_SETTING_UUID" NULLS FIRST);
CREATE INDEX "PROJECT_ALM_SETTINGS_SLUG" ON "PROJECT_ALM_SETTINGS"("ALM_SLUG" NULLS FIRST);
+CREATE INDEX "PROJECT_ALM_SETTINGS_ALM_REPO" ON "PROJECT_ALM_SETTINGS"("ALM_REPO" NULLS FIRST);
CREATE TABLE "PROJECT_BADGE_TOKEN"(
"UUID" CHARACTER VARYING(40) NOT NULL,
@@ -963,7 +967,7 @@ CREATE UNIQUE NULLS NOT DISTINCT INDEX "RULE_TAGS_RULE_UUID" ON "RULE_TAGS"("RUL
CREATE TABLE "RULES"(
"UUID" CHARACTER VARYING(40) NOT NULL,
- "NAME" CHARACTER VARYING(200),
+ "NAME" CHARACTER VARYING(255),
"PLUGIN_RULE_KEY" CHARACTER VARYING(200) NOT NULL,
"PLUGIN_KEY" CHARACTER VARYING(200),
"PLUGIN_CONFIG_KEY" CHARACTER VARYING(200),
@@ -1042,6 +1046,19 @@ CREATE TABLE "SAML_MESSAGE_IDS"(
ALTER TABLE "SAML_MESSAGE_IDS" ADD CONSTRAINT "PK_SAML_MESSAGE_IDS" PRIMARY KEY("UUID");
CREATE UNIQUE NULLS NOT DISTINCT INDEX "SAML_MESSAGE_IDS_UNIQUE" ON "SAML_MESSAGE_IDS"("MESSAGE_ID" NULLS FIRST);
+CREATE TABLE "SCA_ANALYSES"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "COMPONENT_UUID" CHARACTER VARYING(40) NOT NULL,
+ "STATUS" CHARACTER VARYING(40) NOT NULL,
+ "FAILED_REASON" CHARACTER VARYING(255),
+ "ERRORS" CHARACTER LARGE OBJECT NOT NULL,
+ "PARSED_FILES" CHARACTER LARGE OBJECT NOT NULL,
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "SCA_ANALYSES" ADD CONSTRAINT "PK_SCA_ANALYSES" PRIMARY KEY("UUID");
+CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_ANALYSES_COMPONENT_UNIQ" ON "SCA_ANALYSES"("COMPONENT_UUID" NULLS FIRST);
+
CREATE TABLE "SCA_DEPENDENCIES"(
"UUID" CHARACTER VARYING(40) NOT NULL,
"SCA_RELEASE_UUID" CHARACTER VARYING(40) NOT NULL,
@@ -1100,7 +1117,11 @@ CREATE TABLE "SCA_ISSUES_RELEASES"(
"CREATED_AT" BIGINT NOT NULL,
"UPDATED_AT" BIGINT NOT NULL,
"STATUS" CHARACTER VARYING(40) NOT NULL,
- "ASSIGNEE_UUID" CHARACTER VARYING(40)
+ "ASSIGNEE_UUID" CHARACTER VARYING(40),
+ "PREVIOUS_MANUAL_STATUS" CHARACTER VARYING(40),
+ "ORIGINAL_SEVERITY" CHARACTER VARYING(15) NOT NULL,
+ "MANUAL_SEVERITY" CHARACTER VARYING(15),
+ "SHOW_INCREASED_SEVERITY_WARNING" BOOLEAN DEFAULT FALSE NOT NULL
);
ALTER TABLE "SCA_ISSUES_RELEASES" ADD CONSTRAINT "PK_SCA_ISSUES_RELEASES" PRIMARY KEY("UUID");
CREATE INDEX "SCA_ISSUES_RELEASES_SCA_ISSUE" ON "SCA_ISSUES_RELEASES"("SCA_ISSUE_UUID" NULLS FIRST);
@@ -1143,10 +1164,12 @@ CREATE TABLE "SCA_LICENSE_PROFILES"(
"IS_DEFAULT_PROFILE" BOOLEAN NOT NULL,
"NAME" CHARACTER VARYING(400) NOT NULL,
"CREATED_AT" BIGINT NOT NULL,
- "UPDATED_AT" BIGINT NOT NULL
+ "UPDATED_AT" BIGINT NOT NULL,
+ "POLICY_UPDATED_AT" BIGINT NOT NULL,
+ "ORGANIZATION_UUID" CHARACTER VARYING(40) DEFAULT '00000000-0000-4000-0000-000000000000' NOT NULL
);
ALTER TABLE "SCA_LICENSE_PROFILES" ADD CONSTRAINT "PK_SCA_LICENSE_PROFILES" PRIMARY KEY("UUID");
-CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_LICENSE_PROFILES_UNIQ" ON "SCA_LICENSE_PROFILES"("NAME" NULLS FIRST);
+CREATE UNIQUE NULLS NOT DISTINCT INDEX "SCA_LICENSE_PROFILES_UNIQ" ON "SCA_LICENSE_PROFILES"("ORGANIZATION_UUID" NULLS FIRST, "NAME" NULLS FIRST);
CREATE TABLE "SCA_RELEASES"(
"UUID" CHARACTER VARYING(40) NOT NULL,
@@ -1173,7 +1196,8 @@ CREATE TABLE "SCA_VULNERABILITY_ISSUES"(
"CWE_IDS" CHARACTER VARYING(255) NOT NULL,
"CVSS_SCORE" DOUBLE PRECISION,
"CREATED_AT" BIGINT NOT NULL,
- "UPDATED_AT" BIGINT NOT NULL
+ "UPDATED_AT" BIGINT NOT NULL,
+ "WITHDRAWN" BOOLEAN DEFAULT FALSE NOT NULL
);
ALTER TABLE "SCA_VULNERABILITY_ISSUES" ADD CONSTRAINT "PK_SCA_VULNERABILITY_ISSUES" PRIMARY KEY("UUID");
diff --git a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
index 26dcfbab451..d1ee49895e2 100644
--- a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
+++ b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
@@ -208,7 +208,7 @@ public class UserDbTester {
}
public int countAllGroups() {
- return db.getDbClient().groupDao().countByQuery(db.getSession(), new GroupQuery(null, null));
+ return db.getDbClient().groupDao().countByQuery(db.getSession(), new GroupQuery(null, null, null, null));
}
public Optional<ExternalGroupDto> selectExternalGroupByGroupUuid(String groupUuid) {
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphsIT.java
new file mode 100644
index 00000000000..167b797de15
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphsIT.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+
+class AddAnalysisUuidOnArchitectureGraphsIT {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String COLUMN_NAME = "analysis_uuid";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddAnalysisUuidOnArchitectureGraphs.class);
+ private final DdlChange underTest = new AddAnalysisUuidOnArchitectureGraphs(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, VarcharColumnDef.UUID_SIZE, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, VarcharColumnDef.UUID_SIZE, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleasesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleasesIT.java
new file mode 100644
index 00000000000..190c168adca
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleasesIT.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddAssigneeNameToScaIssuesReleasesIT {
+ private static final String TABLE_NAME = "sca_issues_releases";
+ private static final String COLUMN_NAME = "assignee_name";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddAssigneeNameToScaIssuesReleases.class);
+ private final DdlChange underTest = new AddAssigneeNameToScaIssuesReleases(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 200, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 200, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsIT.java
new file mode 100644
index 00000000000..79cf8e29606
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsIT.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+
+class AddGraphVersionOnArchitectureGraphsIT {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String COLUMN_NAME = "graph_version";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddGraphVersionOnArchitectureGraphsTable.class);
+ private final DdlChange underTest = new AddGraphVersionOnArchitectureGraphsTable(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphsIT.java
new file mode 100644
index 00000000000..22f757cd152
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphsIT.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddPerspectiveKeyOnArchitectureGraphsIT {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String COLUMN_NAME = "perspective_key";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddPerspectiveKeyOnArchitectureGraphs.class);
+ private final DdlChange underTest = new AddPerspectiveKeyOnArchitectureGraphs(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTableIT.java
new file mode 100644
index 00000000000..cc4798f73d2
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTableIT.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BIGINT;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddPolicyUpdatedAtToScaLicenseProfilesTableIT {
+ private static final String TABLE_NAME = "sca_license_profiles";
+ private static final String COLUMN_NAME = "policy_updated_at";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddPolicyUpdatedAtToScaLicenseProfilesTable.class);
+ private final DdlChange underTest = new AddPolicyUpdatedAtToScaLicenseProfilesTable(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BIGINT, null, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BIGINT, null, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleasesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleasesIT.java
new file mode 100644
index 00000000000..cf293d4c9bd
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleasesIT.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddPreviousManualStatusToScaIssuesReleasesIT {
+ private static final String TABLE_NAME = "sca_issues_releases";
+ private static final String COLUMN_NAME = "previous_manual_status";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddPreviousManualStatusToScaIssuesReleases.class);
+ private final DdlChange underTest = new AddPreviousManualStatusToScaIssuesReleases(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChangesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChangesIT.java
new file mode 100644
index 00000000000..e8c1735e3df
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChangesIT.java
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import javax.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class BackfillRemoveAssigneeNameFromIssueReleaseChangesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(BackfillRemoveAssigneeNameFromIssueReleaseChanges.class);
+ private final DataChange underTest = new BackfillRemoveAssigneeNameFromIssueReleaseChanges(db.database());
+
+ @Test
+ void execute_withAssigneeName_modifiesRecords() throws SQLException {
+ // Record with only assigneeName field - should result in empty object
+ insertIssueReleaseChange("1", "{\"assigneeName\": [null, \"alice\"]}");
+
+ // Record with mixed fields - should remove only assigneeName
+ insertIssueReleaseChange("2",
+ "{" +
+ "\"assigneeUuid\":[\"user1-uuid\",\"user2-uuid\"]," +
+ "\"assigneeName\":[\"alice\",\"bob\"]}");
+
+ underTest.execute();
+
+ assertThat(getChangeData("1")).isEqualTo("{}");
+ assertThat(getChangeData("2")).isEqualTo("{\"assigneeUuid\":[\"user1-uuid\",\"user2-uuid\"]}");
+ }
+
+ @Test
+ void execute_withoutAssigneeName_doesNotModifyRecords() throws SQLException {
+ var changeData1 = "{\"assigneeUuid\":[\"user1-uuid\",\"user2-uuid\"]}";
+ insertIssueReleaseChange("1", changeData1);
+
+ underTest.execute();
+
+ assertThat(getChangeData("1")).isEqualTo(changeData1);
+ }
+
+ @Test
+ void execute_withNullChangeData_doesNotModifyRecords() throws SQLException {
+ insertIssueReleaseChange("1", null);
+
+ underTest.execute();
+
+ assertThat(getChangeData("1")).isNull();
+ }
+
+ private void insertIssueReleaseChange(String suffix, @Nullable String changeData) {
+ db.executeInsert("sca_issue_rels_changes",
+ "uuid", "scaIssueReleaseChangeUuid" + suffix,
+ "sca_issues_releases_uuid", "scaIssueReleaseUuid" + suffix,
+ "user_uuid", "user1-uuid",
+ "change_data", changeData,
+ "created_at", 1L,
+ "updated_at", 2L,
+ "change_comment", "my comment");
+ }
+
+ private String getChangeData(String suffix) {
+ var uuid = "scaIssueReleaseChangeUuid" + suffix;
+ return (String) db.selectFirst("select change_data from sca_issue_rels_changes where uuid = '" + uuid + "'")
+ .get("change_data");
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTableIT.java
new file mode 100644
index 00000000000..f67d12fb20c
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTableIT.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.CLOB;
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+class CreateScaAnalysesTableIT {
+ private static final String TABLE_NAME = "sca_analyses";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateScaAnalysesTable.class);
+ private final DdlChange underTest = new CreateScaAnalysesTable(db.database());
+
+ @Test
+ void execute_shouldCreateTable() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ db.assertPrimaryKey(TABLE_NAME, "pk_sca_analyses", "uuid");
+ db.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "component_uuid", VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(TABLE_NAME, "status", VARCHAR, 40, false);
+ db.assertColumnDefinition(TABLE_NAME, "failed_reason", VARCHAR, 255, true);
+ db.assertColumnDefinition(TABLE_NAME, "errors", CLOB, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "parsed_files", CLOB, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, null, false);
+ db.assertColumnDefinition(TABLE_NAME, "updated_at", BIGINT, null, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertTableDoesNotExist(TABLE_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertTableExists(TABLE_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphsIT.java
new file mode 100644
index 00000000000..8cd566df9ce
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphsIT.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class CreateUniqueIndexOnArchitectureGraphsIT {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String INDEX_NAME = "uq_idx_ag_brch_tp_src_pspctv";
+ private static final String COLUMN_NAME_BRANCH_UUID = "branch_uuid";
+ private static final String COLUMN_NAME_TYPE = "type";
+ private static final String COLUMN_NAME_ECOSYSTEM = "ecosystem";
+ private static final String COLUMN_NAME_PERSPECTIVE_KEY = "perspective_key";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnArchitectureGraphs.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnArchitectureGraphs(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_BRANCH_UUID, COLUMN_NAME_TYPE, COLUMN_NAME_ECOSYSTEM, COLUMN_NAME_PERSPECTIVE_KEY);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_BRANCH_UUID, COLUMN_NAME_TYPE, COLUMN_NAME_ECOSYSTEM, COLUMN_NAME_PERSPECTIVE_KEY);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalysesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalysesIT.java
new file mode 100644
index 00000000000..7ccc34ec67e
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalysesIT.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaAnalyses.COLUMN_NAME_COMPONENT_UUID;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaAnalyses.INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v202503.CreateUniqueIndexOnScaAnalyses.TABLE_NAME;
+
+class CreateUniqueIndexOnScaAnalysesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnScaAnalyses.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnScaAnalyses(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_COMPONENT_UUID);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_COMPONENT_UUID);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleasesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleasesIT.java
new file mode 100644
index 00000000000..472b716adac
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleasesIT.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class DropAssigneeNameFromScaIssuesReleasesIT {
+ private static final String TABLE_NAME = "sca_issues_releases";
+ private static final String COLUMN_NAME = "assignee_name";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(DropAssigneeNameFromScaIssuesReleases.class);
+ private final DdlChange underTest = new DropAssigneeNameFromScaIssuesReleases(db.database());
+
+ @Test
+ void execute_shouldDropColumn() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 200, true);
+ underTest.execute();
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 200, true);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphsIT.java
new file mode 100644
index 00000000000..646be1f2f7c
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphsIT.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class DropIndexOnArchitectureGraphsIT {
+
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String COLUMN_NAME_BRANCH_UUID = "branch_uuid";
+ private static final String COLUMN_NAME_TYPE = "type";
+ private static final String COLUMN_NAME_SOURCE = "source";
+ private static final String INDEX_NAME = "uq_idx_ag_branch_type_source";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(DropIndexOnArchitectureGraphs.class);
+ private final DropIndexOnArchitectureGraphs underTest = new DropIndexOnArchitectureGraphs(db.database());
+
+ @Test
+ void index_is_dropped() throws SQLException {
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_BRANCH_UUID, COLUMN_NAME_TYPE, COLUMN_NAME_SOURCE);
+
+ underTest.execute();
+
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ }
+
+ @Test
+ void migration_is_reentrant() throws SQLException {
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_BRANCH_UUID, COLUMN_NAME_TYPE, COLUMN_NAME_SOURCE);
+
+ underTest.execute();
+ underTest.execute();
+
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTableIT.java
index 0ee0034af74..1c64c708ad8 100644
--- a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTableIT.java
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaDependenciesTableIT.java
@@ -37,7 +37,7 @@ class DropNewInPullRequestFromScaDependenciesTableIT {
private final DdlChange underTest = new DropNewInPullRequestFromScaDependenciesTable(db.database());
@Test
- void execute_shouldAddColumn() throws SQLException {
+ void execute_shouldDropColumn() throws SQLException {
db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false);
underTest.execute();
db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTableIT.java
index 252537bfcc8..b0a579b14cb 100644
--- a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTableIT.java
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropNewInPullRequestFromScaReleasesTableIT.java
@@ -37,7 +37,7 @@ class DropNewInPullRequestFromScaReleasesTableIT {
private final DdlChange underTest = new DropNewInPullRequestFromScaReleasesTable(db.database());
@Test
- void execute_shouldAddColumn() throws SQLException {
+ void execute_shouldDropColumn() throws SQLException {
db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false);
underTest.execute();
db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicensesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicensesIT.java
new file mode 100644
index 00000000000..d18b45af262
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicensesIT.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.MigrationStep;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class MigrateRemoveNonCanonicalScaEncounteredLicensesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(MigrateRemoveNonCanonicalScaEncounteredLicenses.class);
+ private final MigrationStep underTest = new MigrateRemoveNonCanonicalScaEncounteredLicenses(db.database());
+
+ @Test
+ void test_removesNonCanonical() throws SQLException {
+ // we should keep these
+ insertEncounteredLicense("0", "GPL-2.0-only");
+ insertEncounteredLicense("1", "LicenseRef-something");
+ insertEncounteredLicense("2", "LicenseRef-something-with-something-else");
+
+ // we should delete these
+ insertEncounteredLicense("3", "GPL-2.0-with-classpath-exception");
+ insertEncounteredLicense("4", "GPL-2.0-with-autoconf-exception");
+
+ assertThat(db.countSql("select count(*) from sca_encountered_licenses")).isEqualTo(5);
+ underTest.execute();
+
+ assertThat(db.select("select uuid from sca_encountered_licenses")).map(row -> row.get("uuid"))
+ .containsExactlyInAnyOrder("scaEncounteredLicenseUuid0", "scaEncounteredLicenseUuid1", "scaEncounteredLicenseUuid2");
+ }
+
+ @Test
+ void test_canRunMultipleTimesOnEmptyTable() throws SQLException {
+ assertThat(db.countSql("select count(*) from sca_encountered_licenses")).isZero();
+ underTest.execute();
+ underTest.execute();
+ assertThat(db.countSql("select count(*) from sca_encountered_licenses")).isZero();
+ }
+
+ private void insertEncounteredLicense(String suffix, String licensePolicyId) {
+ db.executeInsert("sca_encountered_licenses",
+ "uuid", "scaEncounteredLicenseUuid" + suffix,
+ "license_policy_id", licensePolicyId,
+ "updated_at", 1L,
+ "created_at", 1L);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTableIT.java
new file mode 100644
index 00000000000..483d8a1afb8
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTableIT.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTableIT {
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.class);
+ private final PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable underTest = new PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable(db.database());
+
+ @Test
+ void execute_shouldPopulatePolicyUpdatedAtWithUpdatedAt() throws SQLException {
+ insertScaLicenseProfile(1);
+ insertScaLicenseProfile(2);
+
+ underTest.execute();
+
+ assertThatPolicyUpdatedAtIsPopulated();
+ }
+
+ @Test
+ void execute_whenAlreadyExecuted_shouldBeIdempotent() throws SQLException {
+ insertScaLicenseProfile(1);
+
+ underTest.execute();
+ underTest.execute();
+
+ assertThatPolicyUpdatedAtIsPopulated();
+ }
+
+ private void insertScaLicenseProfile(Integer index) {
+ db.executeInsert("sca_license_profiles",
+ "uuid", "uuid-" + index,
+ "is_default_profile", false,
+ "name", "licenseProfile-" + index,
+ "created_at", 1L,
+ "updated_at", 2L,
+ "policy_updated_at", null);
+ }
+
+ private void assertThatPolicyUpdatedAtIsPopulated() {
+ List<Map<String, Object>> rows = db.select("select policy_updated_at, updated_at from sca_license_profiles");
+ assertThat(rows).isNotEmpty()
+ .allSatisfy(row -> assertEquals(row.get("policy_updated_at"), row.get("updated_at")));
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRenameIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRenameIT.java
new file mode 100644
index 00000000000..81c377724e9
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRenameIT.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class UpdateArchitectureGraphsSourceColumnRenameIT {
+
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String OLD_COLUMN = "source";
+ private static final String NEW_COLUMN = "ecosystem";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(UpdateArchitectureGraphsSourceColumnRename.class);
+ private final DdlChange underTest = new UpdateArchitectureGraphsSourceColumnRename(db.database());
+
+ @Test
+ void execute_shouldUpdateColumn() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, OLD_COLUMN, VARCHAR, null, null);
+ db.assertColumnDoesNotExist(TABLE_NAME, NEW_COLUMN);
+
+ underTest.execute();
+
+ db.assertColumnDoesNotExist(TABLE_NAME, OLD_COLUMN);
+ db.assertColumnDefinition(TABLE_NAME, NEW_COLUMN, VARCHAR, null, null);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, OLD_COLUMN, VARCHAR, null, null);
+ db.assertColumnDoesNotExist(TABLE_NAME, NEW_COLUMN);
+
+ underTest.execute();
+ underTest.execute();
+
+ db.assertColumnDoesNotExist(TABLE_NAME, OLD_COLUMN);
+ db.assertColumnDefinition(TABLE_NAME, NEW_COLUMN, VARCHAR, null, null);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullableIT.java
new file mode 100644
index 00000000000..4d2d61106be
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullableIT.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.sql.DropColumnsBuilder;
+
+import static java.sql.Types.BIGINT;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+class UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullableIT {
+ static final String TABLE_NAME = "sca_license_profiles";
+ static final String COLUMN_NAME = "policy_updated_at";
+
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.class);
+ private final UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable underTest = new UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable(db.database());
+
+ @Test
+ void execute_whenColumnExists_shouldMakeColumnNotNull() throws SQLException {
+ // Verify column is nullable before update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BIGINT, null, true);
+
+ underTest.execute();
+
+ // Verify column is not nullable after update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BIGINT, null, false);
+ }
+
+ @Test
+ void execute_whenColumnDoesNotExist_shouldNotFail() throws SQLException {
+ // Ensure the column does not exist before executing the migration
+ DropColumnsBuilder dropColumnsBuilder = new DropColumnsBuilder(db.database().getDialect(), TABLE_NAME, COLUMN_NAME);
+ dropColumnsBuilder.build().forEach(db::executeDdl);
+
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ }
+
+ @Test
+ void execute_whenExecutedTwice_shouldBeIdempotent() throws SQLException {
+ underTest.execute();
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BIGINT, null, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettingsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettingsIT.java
new file mode 100644
index 00000000000..eb1afa4db76
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettingsIT.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202504.AddIndexOnAlmRepoInProjectAlmSettings.COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.AddIndexOnAlmRepoInProjectAlmSettings.TABLE_NAME;
+
+class AddIndexOnAlmRepoInProjectAlmSettingsIT {
+
+ private static final String INDEX_NAME = "project_alm_settings_alm_repo";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddIndexOnAlmRepoInProjectAlmSettings.class);
+
+ private final DdlChange underTest = new AddIndexOnAlmRepoInProjectAlmSettings(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME);
+ }
+
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssuesTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssuesTest.java
new file mode 100644
index 00000000000..55558aae959
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssuesTest.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BOOLEAN;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddManualSeverityWarningToScaIssuesTest {
+ private static final String TABLE_NAME = "sca_issues_releases";
+ private static final String COLUMN_NAME = "show_increased_severity_warning";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddManualSeverityWarningToScaIssues.class);
+ private final DdlChange underTest = new AddManualSeverityWarningToScaIssues(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, 1, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, 1, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfilesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfilesIT.java
new file mode 100644
index 00000000000..08f6d1c40de
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfilesIT.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddOrganizationUuidToScaLicenseProfilesIT {
+ private static final String TABLE_NAME = "sca_license_profiles";
+ private static final String COLUMN_NAME = "organization_uuid";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddOrganizationUuidToScaLicenseProfiles.class);
+ private final DdlChange underTest = new AddOrganizationUuidToScaLicenseProfiles(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 40, false);
+ }
+
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java
new file mode 100644
index 00000000000..0fdbda384a9
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.MANUALLY_SET_COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.ORIGINAL_VALUE_COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.TABLE_NAME;
+
+class AddOriginalAndManualSeverityToScaIssuesTest {
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddOriginalAndManualSeverityToScaIssues.class);
+ private final DdlChange underTest = new AddOriginalAndManualSeverityToScaIssues(db.database());
+
+ @Test
+ void execute_shouldAddCalculatedValueColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME, VARCHAR, 15, true);
+ }
+
+ @Test
+ void execute_shouldAddManuallySetColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, MANUALLY_SET_COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, MANUALLY_SET_COLUMN_NAME, VARCHAR, 15, true);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME);
+ db.assertColumnDoesNotExist(TABLE_NAME, MANUALLY_SET_COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME, VARCHAR, 15, true);
+ db.assertColumnDefinition(TABLE_NAME, MANUALLY_SET_COLUMN_NAME, VARCHAR, 15, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssuesTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssuesTest.java
new file mode 100644
index 00000000000..e463be0ad37
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssuesTest.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static java.sql.Types.BOOLEAN;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class AddWithdrawnToScaVulnerabilityIssuesTest {
+ private static final String TABLE_NAME = "sca_vulnerability_issues";
+ private static final String COLUMN_NAME = "withdrawn";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(AddWithdrawnToScaVulnerabilityIssues.class);
+ private final DdlChange underTest = new AddWithdrawnToScaVulnerabilityIssues(db.database());
+
+ @Test
+ void execute_shouldAddColumn() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, 1, false);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, 1, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfilesIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfilesIT.java
new file mode 100644
index 00000000000..c944e1ea1da
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfilesIT.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+import static org.sonar.server.platform.db.migration.version.v202504.CreateUniqueIndexOnScaLicenseProfiles.COLUMN_NAME_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.CreateUniqueIndexOnScaLicenseProfiles.COLUMN_NAME_ORG;
+import static org.sonar.server.platform.db.migration.version.v202504.CreateUniqueIndexOnScaLicenseProfiles.INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v202504.CreateUniqueIndexOnScaLicenseProfiles.TABLE_NAME;
+
+class CreateUniqueIndexOnScaLicenseProfilesIT {
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(org.sonar.server.platform.db.migration.version.v202504.CreateUniqueIndexOnScaLicenseProfiles.class);
+ private final DdlChange underTest = new CreateUniqueIndexOnScaLicenseProfiles(db.database());
+
+ @Test
+ void execute_shouldCreateIndex() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_ORG, COLUMN_NAME_NAME);
+ }
+
+ @Test
+ void execute_shouldBeReentrant() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ underTest.execute();
+ underTest.execute();
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_ORG, COLUMN_NAME_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfilesTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfilesTest.java
new file mode 100644
index 00000000000..799adb2d35d
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfilesTest.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class DropUniqueIndexOnScaLicenseProfilesTest {
+
+ private static final String TABLE_NAME = "sca_license_profiles";
+ private static final String INDEX_NAME = "sca_license_profiles_uniq";
+ private static final String COLUMN_NAME_NAME = "name";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(DropUniqueIndexOnScaLicenseProfiles.class);
+ private final DropUniqueIndexOnScaLicenseProfiles underTest = new DropUniqueIndexOnScaLicenseProfiles(db.database());
+
+ @Test
+ void index_is_dropped() throws SQLException {
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_NAME);
+
+ underTest.execute();
+
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ }
+
+ @Test
+ void migration_is_reentrant() throws SQLException {
+ db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_NAME);
+
+ underTest.execute();
+ underTest.execute();
+
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java
new file mode 100644
index 00000000000..31091c17fc1
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PopulateOriginalSeverityForScaIssuesReleasesTableIT {
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(PopulateOriginalSeverityForScaIssuesReleasesTable.class);
+ private final PopulateOriginalSeverityForScaIssuesReleasesTable underTest = new PopulateOriginalSeverityForScaIssuesReleasesTable(db.database());
+
+ @Test
+ void execute_shouldPopulateOriginaldSeverity() throws SQLException {
+ insertScaIssuesReleases(1, "HIGH", null);
+ insertScaIssuesReleases(2, "INFO", null);
+
+ underTest.execute();
+
+ assertThatOriginalSeverityIs(1, "HIGH");
+ assertThatOriginalSeverityIs(2, "INFO");
+ }
+
+ @Test
+ void execute_whenAlreadyExecuted_shouldBeIdempotent() throws SQLException {
+ insertScaIssuesReleases(1, "HIGH", "INFO");
+
+ underTest.execute();
+ underTest.execute();
+
+ assertThatOriginalSeverityIs(1, "INFO");
+ }
+
+ private void insertScaIssuesReleases(Integer index, String severity, @Nullable String originalSeverity) {
+ db.executeInsert("sca_issues_releases",
+ "uuid", "uuid-" + index,
+ "sca_issue_uuid", "issue_id" + index,
+ "sca_release_uuid", "release_id",
+ "status", "TO_REVIEW",
+ "severity", severity,
+ "original_severity", originalSeverity,
+ "manual_severity", null,
+ "severity_sort_key", 1,
+ "created_at", new Date().getTime(),
+ "updated_at", new Date().getTime()
+ );
+ }
+
+ private void assertThatOriginalSeverityIs(Integer index, String expectedSeverity) {
+ String uuid = "uuid-" + index;
+ List<Map<String, Object>> rows = db.select("select original_severity from sca_issues_releases where uuid = '%s'".formatted(uuid));
+ assertThat(rows).isNotEmpty()
+ .allSatisfy(row -> assertThat(row).containsEntry("original_severity", expectedSeverity));
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSizeTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSizeTest.java
new file mode 100644
index 00000000000..c087c930ef2
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSizeTest.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+
+import static java.sql.Types.VARCHAR;
+import static org.sonar.db.MigrationDbTester.createForMigrationStep;
+
+class UpdateRulesNameColumnSizeTest {
+
+ private static final String TABLE_NAME = "rules";
+ private static final String COLUMN_NAME = "name";
+
+ @RegisterExtension
+ public final MigrationDbTester db = createForMigrationStep(DropUniqueIndexOnScaLicenseProfiles.class);
+ private final UpdateRulesNameColumnSize updateRulesNameColumnSize = new UpdateRulesNameColumnSize(db.database());
+
+ @Test
+ void migration_updates_rules_name_column_size() throws SQLException {
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 200, true);
+
+ updateRulesNameColumnSize.execute();
+
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, true);
+ }
+
+ @Test
+ void migration_is_reentrant() throws SQLException {
+ updateRulesNameColumnSize.execute();
+ updateRulesNameColumnSize.execute();
+
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, 255, true);
+ }
+}
diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java
new file mode 100644
index 00000000000..2e194f52ec6
--- /dev/null
+++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.db.MigrationDbTester;
+import org.sonar.server.platform.db.migration.sql.DropColumnsBuilder;
+
+import static java.sql.Types.VARCHAR;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+class UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "original_severity";
+ static final int COLUMN_SIZE = 15;
+
+ @RegisterExtension
+ public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.class);
+
+ private final UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable underTest = new UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable(db.database());
+
+ @Test
+ void execute_whenColumnExists_shouldMakeColumnNotNull() throws SQLException {
+ // Verify column is nullable before update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, true);
+
+ underTest.execute();
+
+ // Verify column is not nullable after update
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, false);
+ }
+
+ @Test
+ void execute_whenColumnDoesNotExist_shouldNotFail() throws SQLException {
+ // Ensure the column does not exist before executing the migration
+ DropColumnsBuilder dropColumnsBuilder = new DropColumnsBuilder(db.database().getDialect(), TABLE_NAME, COLUMN_NAME);
+ dropColumnsBuilder.build().forEach(db::executeDdl);
+
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ }
+
+ @Test
+ void execute_whenExecutedTwice_shouldBeIdempotent() throws SQLException {
+ underTest.execute();
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, false);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java
index fdfcd97907a..23c17a18540 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java
@@ -31,6 +31,7 @@ import org.sonar.server.platform.db.migration.version.v00.DbVersion00;
import org.sonar.server.platform.db.migration.version.v202501.DbVersion202501;
import org.sonar.server.platform.db.migration.version.v202502.DbVersion202502;
import org.sonar.server.platform.db.migration.version.v202503.DbVersion202503;
+import org.sonar.server.platform.db.migration.version.v202504.DbVersion202504;
public class MigrationConfigurationModule extends Module {
@Override
@@ -42,6 +43,7 @@ public class MigrationConfigurationModule extends Module {
DbVersion202501.class,
DbVersion202502.class,
DbVersion202503.class,
+ DbVersion202504.class,
// migration steps
MigrationStepRegistryImpl.class,
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java
index 802396d18b1..2664ff7c0e9 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java
@@ -46,15 +46,26 @@ public class CreateUniqueIndexOnArchitectureGraphs extends DdlChange {
}
private void createIndex(Context context, Connection connection) {
- if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
- context.execute(new CreateIndexBuilder(getDialect())
- .setTable(TABLE_NAME)
- .setName(INDEX_NAME)
- .setUnique(true)
- .addColumn(COLUMN_NAME_BRANCH_UUID, false)
- .addColumn(COLUMN_NAME_TYPE, false)
- .addColumn(COLUMN_NAME_SOURCE, false)
- .build());
+ if(!DatabaseUtils.tableColumnExists(connection, TABLE_NAME, COLUMN_NAME_BRANCH_UUID)) {
+ return;
}
+ if(!DatabaseUtils.tableColumnExists(connection, TABLE_NAME, COLUMN_NAME_TYPE)) {
+ return;
+ }
+ if(!DatabaseUtils.tableColumnExists(connection, TABLE_NAME, COLUMN_NAME_SOURCE)) {
+ return;
+ }
+ if (DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ return;
+ }
+
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_NAME_BRANCH_UUID, false)
+ .addColumn(COLUMN_NAME_TYPE, false)
+ .addColumn(COLUMN_NAME_SOURCE, false)
+ .build());
}
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphs.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphs.java
new file mode 100644
index 00000000000..1110d46c570
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAnalysisUuidOnArchitectureGraphs.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddAnalysisUuidOnArchitectureGraphs extends DdlChange {
+ static final String TABLE_NAME = "architecture_graphs";
+ static final String COLUMN_NAME = "analysis_uuid";
+
+ public AddAnalysisUuidOnArchitectureGraphs(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(DdlChange.Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(VarcharColumnDef.UUID_SIZE)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleases.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleases.java
new file mode 100644
index 00000000000..a74272f4b2c
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddAssigneeNameToScaIssuesReleases.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddAssigneeNameToScaIssuesReleases extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "assignee_name";
+
+ public AddAssigneeNameToScaIssuesReleases(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(200)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsTable.java
new file mode 100644
index 00000000000..55ca4c73a4f
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddGraphVersionOnArchitectureGraphsTable.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddGraphVersionOnArchitectureGraphsTable extends DdlChange {
+ static final String TABLE_NAME = "architecture_graphs";
+ static final String COLUMN_NAME = "graph_version";
+
+ public AddGraphVersionOnArchitectureGraphsTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(DdlChange.Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(255)
+ .setIsNullable(false)
+ .setDefaultValue("1.0.0")
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphs.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphs.java
new file mode 100644
index 00000000000..994e05650a0
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPerspectiveKeyOnArchitectureGraphs.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddPerspectiveKeyOnArchitectureGraphs extends DdlChange {
+ static final String TABLE_NAME = "architecture_graphs";
+ static final String COLUMN_NAME = "perspective_key";
+
+ public AddPerspectiveKeyOnArchitectureGraphs(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(255)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTable.java
new file mode 100644
index 00000000000..cd6b6e76cc5
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPolicyUpdatedAtToScaLicenseProfilesTable.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BigIntegerColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddPolicyUpdatedAtToScaLicenseProfilesTable extends DdlChange {
+ static final String TABLE_NAME = "sca_license_profiles";
+ static final String COLUMN_NAME = "policy_updated_at";
+
+ public AddPolicyUpdatedAtToScaLicenseProfilesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = BigIntegerColumnDef.newBigIntegerColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleases.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleases.java
new file mode 100644
index 00000000000..fb5514c42e9
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddPreviousManualStatusToScaIssuesReleases.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddPreviousManualStatusToScaIssuesReleases extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "previous_manual_status";
+
+ public AddPreviousManualStatusToScaIssuesReleases(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(40)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChanges.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChanges.java
new file mode 100644
index 00000000000..762c9e90834
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/BackfillRemoveAssigneeNameFromIssueReleaseChanges.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonParser;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class BackfillRemoveAssigneeNameFromIssueReleaseChanges extends DataChange {
+ // Select records that might contain assigneeName in their change_data
+ private static final String SELECT_QUERY = "select uuid, change_data from sca_issue_rels_changes where change_data like '%assigneeName%'";
+ private static final String UPDATE_QUERY = "update sca_issue_rels_changes set change_data = ? where uuid = ?";
+ private static final Gson GSON = new Gson();
+
+ public BackfillRemoveAssigneeNameFromIssueReleaseChanges(Database db) {
+ super(db);
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select(SELECT_QUERY);
+ massUpdate.update(UPDATE_QUERY);
+
+ massUpdate.execute((row, update, index) -> {
+ String uuid = row.getString(1);
+ String changeData = row.getString(2);
+
+ if (changeData == null) {
+ return false;
+ }
+
+ String updatedChangeData = removeAssigneeNameFromChangeData(changeData);
+
+ if (updatedChangeData.equals(changeData)) {
+ return false;
+ }
+ update.setString(1, updatedChangeData);
+ update.setString(2, uuid);
+ return true;
+ });
+ }
+
+ private static String removeAssigneeNameFromChangeData(String changeData) {
+ var obj = JsonParser.parseString(changeData).getAsJsonObject();
+ obj.remove("assigneeName");
+ return GSON.toJson(obj);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTable.java
new file mode 100644
index 00000000000..1356126259a
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateScaAnalysesTable.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import java.util.List;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.CreateTableChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.ClobColumnDef.newClobColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class CreateScaAnalysesTable extends CreateTableChange {
+ public static final String TABLE_NAME = "sca_analyses";
+ public static final int STATUS_COLUMN_SIZE = 40;
+ public static final int FAILED_REASON_COLUMN_SIZE = 255;
+
+ protected CreateScaAnalysesTable(Database db) {
+ super(db, TABLE_NAME);
+ }
+
+ @Override
+ public void execute(Context context, String tableName) throws SQLException {
+ List<String> createQuery = new CreateTableBuilder(getDialect(), tableName)
+ .addPkColumn(newVarcharColumnDefBuilder().setColumnName("uuid").setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("component_uuid").setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("status").setIsNullable(false).setLimit(STATUS_COLUMN_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("failed_reason").setIsNullable(true).setLimit(FAILED_REASON_COLUMN_SIZE).build())
+ .addColumn(newClobColumnDefBuilder().setColumnName("errors").setIsNullable(false).build())
+ .addColumn(newClobColumnDefBuilder().setColumnName("parsed_files").setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName("created_at").setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName("updated_at").setIsNullable(false).build())
+ .build();
+
+ context.execute(createQuery);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphs.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphs.java
new file mode 100644
index 00000000000..1be744195c9
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnArchitectureGraphs.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnArchitectureGraphs extends DdlChange {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String INDEX_NAME = "uq_idx_ag_brch_tp_src_pspctv";
+ private static final String COLUMN_NAME_BRANCH_UUID = "branch_uuid";
+ private static final String COLUMN_NAME_TYPE = "type";
+ private static final String COLUMN_NAME_ECOSYSTEM = "ecosystem";
+ private static final String COLUMN_NAME_PERSPECTIVE_KEY = "perspective_key";
+
+ public CreateUniqueIndexOnArchitectureGraphs(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_NAME_BRANCH_UUID, false)
+ .addColumn(COLUMN_NAME_TYPE, false)
+ .addColumn(COLUMN_NAME_ECOSYSTEM, false)
+ .addColumn(COLUMN_NAME_PERSPECTIVE_KEY, true) // nullable
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalyses.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalyses.java
new file mode 100644
index 00000000000..0a82ad81e4b
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateUniqueIndexOnScaAnalyses.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnScaAnalyses extends DdlChange {
+ static final String TABLE_NAME = "sca_analyses";
+ static final String INDEX_NAME = "sca_analyses_component_uniq";
+ static final String COLUMN_NAME_COMPONENT_UUID = "component_uuid";
+
+ public CreateUniqueIndexOnScaAnalyses(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_NAME_COMPONENT_UUID, false)
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java
index 1d95a39afb6..ba743de7f8f 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java
@@ -66,6 +66,22 @@ public class DbVersion202503 implements DbVersion {
.add(2025_03_027, "Drop change_type from SCA issues releases changes", DropChangeTypeFromScaIssuesReleasesChangesTable.class)
.add(2025_03_028, "Remove duplicates from SCA releases table", MigrateRemoveDuplicateScaReleases.class)
.add(2025_03_029, "Create unique index on SCA releases table", CreateUniqueIndexOnScaReleases.class)
+ .add(2025_03_030, "Create SCA analyses table", CreateScaAnalysesTable.class)
+ .add(2025_03_031, "Create unique index on SCA analyses table", CreateUniqueIndexOnScaAnalyses.class)
+ .add(2025_03_032, "Add 'analysis_uuid' column to 'architecture_graphs' table", AddAnalysisUuidOnArchitectureGraphs.class)
+ .add(2025_03_033, "Add 'perspective_key' column to 'architecture_graphs' table", AddPerspectiveKeyOnArchitectureGraphs.class)
+ .add(2025_03_034, "Drop unique index on 'architecture_graphs' table", DropIndexOnArchitectureGraphs.class)
+ .add(2025_03_035, "Rename column 'source' to 'ecosystem' on 'architecture_graphs' table", UpdateArchitectureGraphsSourceColumnRename.class)
+ .add(2025_03_036, "Create unique index on 'architecture_graphs' table", CreateUniqueIndexOnArchitectureGraphs.class)
+ .add(2025_03_037, "Add previous_manual_status to SCA issues releases", AddPreviousManualStatusToScaIssuesReleases.class)
+ .add(2025_03_038, "Add 'graph_version' column to 'architecture_graphs' table", AddGraphVersionOnArchitectureGraphsTable.class)
+ .add(2025_03_039, "Add assignee name to SCA issues releases", AddAssigneeNameToScaIssuesReleases.class)
+ .add(2025_03_040, "Drop assignee name from SCA issues releases", DropAssigneeNameFromScaIssuesReleases.class)
+ .add(2025_03_041, "Remove AssigneeName from Sca issue release changes", BackfillRemoveAssigneeNameFromIssueReleaseChanges.class)
+ .add(2025_03_042, "Remove non-canonical from Sca encountered licenses", MigrateRemoveNonCanonicalScaEncounteredLicenses.class)
+ .add(2025_03_043, "Add 'policy_updated_at' to Sca license profiles", AddPolicyUpdatedAtToScaLicenseProfilesTable.class)
+ .add(2025_03_044, "Populate 'policy_updated_at' column for Sca license profiles", PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.class)
+ .add(2025_03_045, "Update 'policy_updated_at' column for Sca license profiles to be not nullable", UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.class)
;
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleases.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleases.java
new file mode 100644
index 00000000000..cfe9ba322b8
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropAssigneeNameFromScaIssuesReleases.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DropColumnChange;
+
+public class DropAssigneeNameFromScaIssuesReleases extends DropColumnChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "assignee_name";
+
+ public DropAssigneeNameFromScaIssuesReleases(Database db) {
+ super(db, TABLE_NAME, COLUMN_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphs.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphs.java
new file mode 100644
index 00000000000..d48236d6d76
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnArchitectureGraphs.java
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DropIndexChange;
+
+public class DropIndexOnArchitectureGraphs extends DropIndexChange {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String INDEX_NAME = "uq_idx_ag_branch_type_source";
+
+ public DropIndexOnArchitectureGraphs(Database db) {
+ super(db, INDEX_NAME, TABLE_NAME);
+ }
+
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicenses.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicenses.java
new file mode 100644
index 00000000000..f9fc92aab5a
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/MigrateRemoveNonCanonicalScaEncounteredLicenses.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.MigrationStep;
+
+public class MigrateRemoveNonCanonicalScaEncounteredLicenses implements MigrationStep {
+ static final String SELECT_BATCH_QUERY = """
+ SELECT
+ uuid
+ FROM sca_encountered_licenses
+ WHERE
+ license_policy_id not like 'LicenseRef%'
+ and license_policy_id like '%-with-%'
+ """;
+
+ static final String DELETE_BATCH_ENCOUNTERED_LICENSES = """
+ DELETE FROM sca_encountered_licenses WHERE uuid IN (?)
+ """;
+
+ private final Database db;
+
+ public MigrateRemoveNonCanonicalScaEncounteredLicenses(Database db) {
+ this.db = db;
+ }
+
+ private static List<String> findBatchOfNonCanonical(Connection connection) throws SQLException {
+ List<String> results = new ArrayList<>();
+
+ try (PreparedStatement preparedStatement = connection.prepareStatement(SELECT_BATCH_QUERY)) {
+ preparedStatement.setMaxRows(999);
+ try (ResultSet resultSet = preparedStatement.executeQuery()) {
+ while (resultSet.next()) {
+ results.add(resultSet.getString(1));
+ }
+ }
+ }
+
+ return results;
+ }
+
+ private static void deleteBatch(Connection connection, List<String> nonCanonicalRowUuids) throws SQLException {
+ try (PreparedStatement preparedStatement = connection.prepareStatement(DELETE_BATCH_ENCOUNTERED_LICENSES)) {
+ for (String uuid : nonCanonicalRowUuids) {
+ preparedStatement.setString(1, uuid);
+ preparedStatement.addBatch();
+ }
+ preparedStatement.executeBatch();
+ }
+ }
+
+ private static void deleteBatchOfNonCanonical(Connection connection, List<String> nonCanonicalRowUuids) throws SQLException {
+ deleteBatch(connection, nonCanonicalRowUuids);
+ }
+
+ @Override
+ public void execute() throws SQLException {
+ try (var connection = db.getDataSource().getConnection()) {
+ List<String> nonCanonicalRowIds = findBatchOfNonCanonical(connection);
+ while (!nonCanonicalRowIds.isEmpty()) {
+ deleteBatchOfNonCanonical(connection, nonCanonicalRowIds);
+ nonCanonicalRowIds = findBatchOfNonCanonical(connection);
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.java
new file mode 100644
index 00000000000..8cacf1212e4
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable extends DataChange {
+ private static final String SELECT_QUERY = "select updated_at, uuid from sca_license_profiles where policy_updated_at is null";
+ private static final String UPDATE_QUERY = "update sca_license_profiles set policy_updated_at = ? where uuid = ?";
+
+ public PopulatePolicyUpdatedAtColumnForScaLicenseProfilesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ protected void execute(DataChange.Context context) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select(SELECT_QUERY);
+ massUpdate.update(UPDATE_QUERY);
+
+ massUpdate.execute((row, update, index) -> {
+ update
+ // Set policy_updated_at from updated_at
+ .setLong(1, row.getLong(1))
+ // Set uuid from uuid
+ .setString(2, row.getString(2));
+ return true;
+ });
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRename.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRename.java
new file mode 100644
index 00000000000..0a19b0046f9
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateArchitectureGraphsSourceColumnRename.java
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.RenameVarcharColumnChange;
+
+public class UpdateArchitectureGraphsSourceColumnRename extends RenameVarcharColumnChange {
+ private static final String TABLE_NAME = "architecture_graphs";
+ private static final String OLD_COLUMN_NAME = "source";
+ private static final String NEW_COLUMN_NAME = "ecosystem";
+
+ public UpdateArchitectureGraphsSourceColumnRename(Database db) {
+ super(db, TABLE_NAME, OLD_COLUMN_NAME, NEW_COLUMN_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.java
new file mode 100644
index 00000000000..44761a449fc
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202503;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BigIntegerColumnDef;
+import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable extends DdlChange {
+ static final String TABLE_NAME = "sca_license_profiles";
+ static final String COLUMN_NAME = "policy_updated_at";
+
+ public UpdateScaLicenseProfilesPolicyUpdatedAtColumnNotNullable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = BigIntegerColumnDef.newBigIntegerColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setIsNullable(false)
+ .build();
+
+ context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME)
+ .updateColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettings.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettings.java
new file mode 100644
index 00000000000..62cde95315b
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddIndexOnAlmRepoInProjectAlmSettings.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.CreateIndexOnColumn;
+
+public class AddIndexOnAlmRepoInProjectAlmSettings extends CreateIndexOnColumn {
+
+ protected static final String TABLE_NAME = "project_alm_settings";
+ protected static final String COLUMN_NAME = "alm_repo";
+ protected static final boolean UNIQUE = false;
+
+ public AddIndexOnAlmRepoInProjectAlmSettings(Database db) {
+ super(db, TABLE_NAME, COLUMN_NAME, UNIQUE);
+ }
+
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssues.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssues.java
new file mode 100644
index 00000000000..9700e9fe565
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddManualSeverityWarningToScaIssues.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BooleanColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddManualSeverityWarningToScaIssues extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "show_increased_severity_warning";
+
+ public AddManualSeverityWarningToScaIssues(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = BooleanColumnDef.newBooleanColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setDefaultValue(false)
+ .setIsNullable(false)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfiles.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfiles.java
new file mode 100644
index 00000000000..90c1cb6e472
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOrganizationUuidToScaLicenseProfiles.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddOrganizationUuidToScaLicenseProfiles extends DdlChange {
+ static final String TABLE_NAME = "sca_license_profiles";
+ static final String COLUMN_NAME = "organization_uuid";
+ static final String SQS_ORGANIZATION_UUID = "00000000-0000-4000-0000-000000000000";
+
+ public AddOrganizationUuidToScaLicenseProfiles(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setDefaultValue(SQS_ORGANIZATION_UUID)
+ .setColumnName(COLUMN_NAME)
+ .setLimit(40)
+ .setIsNullable(false)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java
new file mode 100644
index 00000000000..66c4c27254e
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddOriginalAndManualSeverityToScaIssues extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String ORIGINAL_VALUE_COLUMN_NAME = "original_severity";
+ static final String MANUALLY_SET_COLUMN_NAME = "manual_severity";
+
+ public AddOriginalAndManualSeverityToScaIssues(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(ORIGINAL_VALUE_COLUMN_NAME)
+ .setLimit(15)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+
+ if (!tableColumnExists(connection, TABLE_NAME, MANUALLY_SET_COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(MANUALLY_SET_COLUMN_NAME)
+ .setLimit(15)
+ .setIsNullable(true)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssues.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssues.java
new file mode 100644
index 00000000000..786259b8f97
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddWithdrawnToScaVulnerabilityIssues.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BooleanColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class AddWithdrawnToScaVulnerabilityIssues extends DdlChange {
+ static final String TABLE_NAME = "sca_vulnerability_issues";
+ static final String COLUMN_NAME = "withdrawn";
+
+ public AddWithdrawnToScaVulnerabilityIssues(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (!tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = BooleanColumnDef.newBooleanColumnDefBuilder()
+ .setDefaultValue(false)
+ .setColumnName(COLUMN_NAME)
+ .setIsNullable(false)
+ .build();
+
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfiles.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfiles.java
new file mode 100644
index 00000000000..5b58a91fe31
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/CreateUniqueIndexOnScaLicenseProfiles.java
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class CreateUniqueIndexOnScaLicenseProfiles extends DdlChange {
+
+ static final String TABLE_NAME = "sca_license_profiles";
+ static final String INDEX_NAME = "sca_license_profiles_uniq";
+ static final String COLUMN_NAME_ORG = "organization_uuid";
+ static final String COLUMN_NAME_NAME = "name";
+
+ public CreateUniqueIndexOnScaLicenseProfiles(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ createIndex(context, connection);
+ }
+ }
+
+ private void createIndex(Context context, Connection connection) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) {
+ context.execute(new CreateIndexBuilder(getDialect())
+ .setTable(TABLE_NAME)
+ .setName(INDEX_NAME)
+ .setUnique(true)
+ .addColumn(COLUMN_NAME_ORG, true)
+ .addColumn(COLUMN_NAME_NAME, false)
+ .build());
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java
new file mode 100644
index 00000000000..5008209c84a
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import org.sonar.server.platform.db.migration.step.MigrationStepRegistry;
+import org.sonar.server.platform.db.migration.version.DbVersion;
+
+public class DbVersion202504 implements DbVersion {
+ // ignoring bad number formatting, as it's intended that we align the migration numbers to SQ versions
+ @SuppressWarnings("java:S3937")
+
+ @Override
+ public void addSteps(MigrationStepRegistry registry) {
+ registry
+ .add(2025_04_000, "Add 'withdrawn' column to 'sca_vulnerability_issues' table", AddWithdrawnToScaVulnerabilityIssues.class)
+ .add(2025_04_001, "Add 'organization_uuid' column to 'sca_license_profiles' table", AddOrganizationUuidToScaLicenseProfiles.class)
+ .add(2025_04_002, "Drop unique index from 'sca_license_profiles'", DropUniqueIndexOnScaLicenseProfiles.class)
+ .add(2025_04_003, "Create unique index from 'sca_license_profiles'", CreateUniqueIndexOnScaLicenseProfiles.class)
+ .add(2025_04_004, "Add index on 'alm_repo' column in 'project_alm_settings' table", AddIndexOnAlmRepoInProjectAlmSettings.class)
+ .add(2025_04_005, "Add 'original_severity' and 'manual_severity' columns to 'sca_issues_releases' table", AddOriginalAndManualSeverityToScaIssues.class)
+ .add(2025_04_006, "Populate 'original_severity' column for 'sca_issues_releases' table", PopulateOriginalSeverityForScaIssuesReleasesTable.class)
+ .add(2025_04_007, "Update 'original_severity' column to be not nullable in 'sca_issues_releases' table", UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.class)
+ .add(2025_04_008, "Update size of 'name' column in 'rules' table", UpdateRulesNameColumnSize.class)
+ .add(2025_04_009, "Add 'show_increased_severity_warning' column to 'sca_issues_releases' table", AddManualSeverityWarningToScaIssues.class);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfiles.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfiles.java
new file mode 100644
index 00000000000..9294bd95c12
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DropUniqueIndexOnScaLicenseProfiles.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DropIndexChange;
+
+public class DropUniqueIndexOnScaLicenseProfiles extends DropIndexChange {
+ private static final String TABLE_NAME = "sca_license_profiles";
+ private static final String INDEX_NAME = "sca_license_profiles_uniq";
+
+ public DropUniqueIndexOnScaLicenseProfiles(Database db) {
+ super(db, INDEX_NAME, TABLE_NAME);
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java
new file mode 100644
index 00000000000..bcee5bb0903
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class PopulateOriginalSeverityForScaIssuesReleasesTable extends DataChange {
+ private static final String SELECT_QUERY = "select severity, uuid from sca_issues_releases where original_severity is null";
+ private static final String UPDATE_QUERY = "update sca_issues_releases set original_severity = ? where uuid = ?";
+
+ public PopulateOriginalSeverityForScaIssuesReleasesTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select(SELECT_QUERY);
+ massUpdate.update(UPDATE_QUERY);
+
+ massUpdate.execute((row, update, index) -> {
+ update
+ // Set original_severity from severity
+ .setString(1, row.getString(1))
+ // Set uuid from uuid
+ .setString(2, row.getString(2));
+ return true;
+ });
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSize.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSize.java
new file mode 100644
index 00000000000..c93096e5293
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateRulesNameColumnSize.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class UpdateRulesNameColumnSize extends DdlChange {
+ private static final String TABLE_NAME = "rules";
+ private static final String COLUMN_NAME = "name";
+ private static final int NEW_COLUMN_SIZE = 255;
+
+ public UpdateRulesNameColumnSize(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setLimit(NEW_COLUMN_SIZE)
+ .build();
+ context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME)
+ .updateColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java
new file mode 100644
index 00000000000..4c842e81e1f
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.db.DatabaseUtils.tableColumnExists;
+
+public class UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable extends DdlChange {
+ static final String TABLE_NAME = "sca_issues_releases";
+ static final String COLUMN_NAME = "original_severity";
+
+ public UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (var connection = getDatabase().getDataSource().getConnection()) {
+ if (tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) {
+ var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName(COLUMN_NAME)
+ .setIsNullable(false)
+ .setLimit(15)
+ .build();
+
+ context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME)
+ .updateColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/package-info.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/package-info.java
new file mode 100644
index 00000000000..ebad6a23ac3
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.platform.db.migration.version.v202504;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504Test.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504Test.java
new file mode 100644
index 00000000000..3b27807db59
--- /dev/null
+++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504Test.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.db.migration.version.v202504;
+
+import org.junit.jupiter.api.Test;
+
+import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMigrationNotEmpty;
+import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMinimumMigrationNumber;
+
+class DbVersion202503Test {
+
+ private final DbVersion202504 underTest = new DbVersion202504();
+
+ @Test
+ void migrationNumber_starts_at_2025_04_000() {
+ verifyMinimumMigrationNumber(underTest, 2025_04_000);
+ }
+
+ @Test
+ void verify_migration_is_not_empty() {
+ verifyMigrationNotEmpty(underTest);
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java
index 2cb04f5c783..b9813eb1a08 100644
--- a/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java
+++ b/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java
@@ -33,6 +33,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -222,9 +223,14 @@ public class ClusterAppStateImpl implements ClusterAppState {
}
private boolean isElasticSearchOperational() {
- return esConnector.getClusterHealthStatus()
- .filter(t -> ClusterHealthStatus.GREEN.equals(t) || ClusterHealthStatus.YELLOW.equals(t))
- .isPresent();
+ try {
+ return esConnector.getClusterHealthStatus()
+ .filter(t -> ClusterHealthStatus.GREEN.equals(t) || ClusterHealthStatus.YELLOW.equals(t))
+ .isPresent();
+ } catch (ElasticsearchException e) {
+ LOGGER.warn("Cannot check at current time whether Elasticsearch is operational", e);
+ return false;
+ }
}
private void asyncWaitForEsToBecomeOperational() {
diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java
index 73aad40a306..c61a1d3621d 100644
--- a/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java
+++ b/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java
@@ -21,6 +21,7 @@ package org.sonar.application.cluster;
import java.net.InetAddress;
import java.util.Optional;
+import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.junit.Rule;
import org.junit.Test;
@@ -39,6 +40,7 @@ import org.sonar.process.cluster.hz.JoinConfigurationType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@@ -106,6 +108,18 @@ public class ClusterAppStateImplTest {
}
@Test
+ public void isOperational_whenElasticsearchException_tryAgain() {
+ EsConnector esConnectorMock = mock();
+ doThrow(new ElasticsearchException("failed to connect")).when(esConnectorMock).getClusterHealthStatus();
+
+ try (ClusterAppStateImpl underTest = createClusterAppState(esConnectorMock)) {
+ boolean operational = underTest.isOperational(ProcessId.ELASTICSEARCH, false);
+
+ assertThat(operational).isFalse();
+ }
+ }
+
+ @Test
public void constructor_checks_appNodesClusterHostsConsistency() {
AppNodesClusterHostsConsistency clusterHostsConsistency = mock(AppNodesClusterHostsConsistency.class);
try (ClusterAppStateImpl underTest = new ClusterAppStateImpl(new TestAppSettings(), newHzMember(),
diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java
index 3cf08d4085f..fd13abb2bfb 100644
--- a/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java
@@ -101,7 +101,10 @@ public class ProcessProperties {
WEB_HTTP_MAX_THREADS("sonar.web.http.maxThreads"),
WEB_HTTP_ACCEPT_COUNT("sonar.web.http.acceptCount"),
WEB_HTTP_KEEP_ALIVE_TIMEOUT("sonar.web.http.keepAliveTimeout"),
- WEB_SESSION_TIMEOUT_IN_MIN("sonar.web.sessionTimeoutInMinutes"),
+ // The time a user can remain idle (no activity) before the session ends.
+ WEB_INACTIVE_SESSION_TIMEOUT_IN_MIN("sonar.web.sessionTimeoutInMinutes"),
+ // The time a user can remain logged in, regardless of activity
+ WEB_ACTIVE_SESSION_TIMEOUT_IN_MIN("sonar.web.activeSessionTimeoutInMinutes"),
WEB_SYSTEM_PASS_CODE("sonar.web.systemPasscode"),
WEB_ACCESSLOGS_ENABLE("sonar.web.accessLogs.enable"),
WEB_ACCESSLOGS_PATTERN("sonar.web.accessLogs.pattern"),
diff --git a/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java b/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java
index e898a27dbb7..2f624caebef 100644
--- a/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java
+++ b/server/sonar-server-common/src/it/java/org/sonar/server/component/index/EntityDefinitionIndexerIT.java
@@ -21,10 +21,14 @@ package org.sonar.server.component.index;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Optional;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.slf4j.event.Level;
+import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.api.utils.System2;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
@@ -33,15 +37,18 @@ import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ProjectData;
import org.sonar.db.entity.EntityDto;
import org.sonar.db.es.EsQueueDto;
+import org.sonar.db.portfolio.PortfolioDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.server.es.EsClient;
import org.sonar.server.es.EsTester;
import org.sonar.server.es.Indexers;
import org.sonar.server.es.IndexingResult;
+import static java.lang.String.format;
import static java.util.Collections.emptySet;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatException;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
import static org.sonar.db.component.ComponentQualifiers.PROJECT;
import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_NAME;
@@ -60,10 +67,18 @@ public class EntityDefinitionIndexerIT {
public EsTester es = EsTester.create();
@Rule
public DbTester db = DbTester.create(system2);
+ @Rule
+ public LogTester logTester = new LogTester();
private DbClient dbClient = db.getDbClient();
private DbSession dbSession = db.getSession();
- private EntityDefinitionIndexer underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client());
+ private EntityDefinitionIndexer underTest;
+
+ @Before
+ public void setup() {
+ underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client());
+ logTester.setLevel(Level.DEBUG);
+ }
@Test
public void test_getIndexTypes() {
@@ -121,6 +136,62 @@ public class EntityDefinitionIndexerIT {
}
@Test
+ public void indexOnStartup_fixes_corrupted_portfolios_if_possible_and_then_indexes_them() throws Exception {
+ underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client());
+ String uuid = "portfolioUuid1";
+ ProjectDto project = db.components().insertPrivateProject().getProjectDto();
+ PortfolioDto corruptedPortfolio = new PortfolioDto()
+ .setKey("portfolio1")
+ .setName("My Portfolio")
+ .setSelectionMode(PortfolioDto.SelectionMode.NONE)
+ .setUuid(uuid)
+ .setRootUuid(uuid);
+ db.getDbClient().portfolioDao().insert(dbSession, corruptedPortfolio, false);
+
+ // corrupt the portfolio in a fixable way (root portfolio with self-referential parent_uuid)
+ dbSession.getSqlSession().getConnection().prepareStatement(format("UPDATE portfolios SET parent_uuid = '%s' where uuid = '%s'", uuid, uuid))
+ .execute();
+ dbSession.commit();
+ Optional<EntityDto> entity = dbClient.entityDao().selectByUuid(dbSession, uuid);
+
+ assertThat(entity).isPresent();
+ assertThat(entity.get().getAuthUuid()).isNull();
+
+ underTest.indexOnStartup(emptySet());
+
+ assertThat(logTester.logs()).contains("Fixing corrupted portfolio tree for root portfolio " + corruptedPortfolio.getUuid());
+ assertThatIndexContainsOnly(project, corruptedPortfolio);
+ }
+
+ @Test
+ public void indexOnStartup_logs_warning_about_corrupted_portfolios_that_cannot_be_fixed_automatically() throws Exception {
+ underTest = new EntityDefinitionIndexer(db.getDbClient(), es.client());
+ String uuid = "portfolioUuid1";
+ PortfolioDto corruptedPortfolio = new PortfolioDto()
+ .setKey("portfolio1")
+ .setName("My Portfolio")
+ .setSelectionMode(PortfolioDto.SelectionMode.NONE)
+ .setUuid(uuid)
+ .setRootUuid(uuid);
+ db.getDbClient().portfolioDao().insert(dbSession, corruptedPortfolio, false);
+
+ // corrupt the portfolio in an un-fixable way (non-existent parent)
+ dbSession.getSqlSession().getConnection().prepareStatement(format("UPDATE portfolios SET parent_uuid = 'junk_uuid' where uuid = '%s'", uuid))
+ .execute();
+ dbSession.commit();
+ Optional<EntityDto> entity = dbClient.entityDao().selectByUuid(dbSession, uuid);
+
+ assertThat(entity).isPresent();
+ assertThat(entity.get().getAuthUuid()).isNull();
+
+ assertThatException()
+ .isThrownBy(() -> underTest.indexOnStartup(emptySet()));
+
+ assertThat(logTester.logs()).contains("Detected portfolio tree corruption for portfolio " + corruptedPortfolio.getUuid());
+
+ }
+
+ @Test
public void indexOnAnalysis_indexes_project() {
ProjectData project = db.components().insertPrivateProject();
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java b/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java
index 9a9a4cb96f3..a2a2a55ae9c 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/component/index/EntityDefinitionIndexer.java
@@ -30,6 +30,8 @@ import java.util.stream.Collectors;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
@@ -56,7 +58,7 @@ import static org.sonar.server.component.index.ComponentIndexDefinition.TYPE_COM
* Indexes the definition of all entities: projects, applications, portfolios and sub-portfolios.
*/
public class EntityDefinitionIndexer implements EventIndexer, AnalysisIndexer, NeedAuthorizationIndexer {
-
+ private static final Logger LOG = LoggerFactory.getLogger(EntityDefinitionIndexer.class);
private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(TYPE_COMPONENT, entity -> true);
private static final Set<IndexType> INDEX_TYPES = Set.of(TYPE_COMPONENT);
@@ -172,16 +174,43 @@ public class EntityDefinitionIndexer implements EventIndexer, AnalysisIndexer, N
private void doIndexByEntityUuid(Size bulkSize) {
BulkIndexer bulk = new BulkIndexer(esClient, TYPE_COMPONENT, bulkSize);
bulk.start();
+ Set<EntityDto> corruptedEntities = new HashSet<>();
try (DbSession dbSession = dbClient.openSession(false)) {
dbClient.entityDao().scrollForIndexing(dbSession, context -> {
EntityDto dto = context.getResultObject();
- bulk.add(toDocument(dto).toIndexRequest());
+ if (dto.getAuthUuid() == null) {
+ corruptedEntities.add(dto);
+ } else {
+ bulk.add(toDocument(dto).toIndexRequest());
+ }
});
+ if (!corruptedEntities.isEmpty()) {
+ attemptToFixCorruptedEntities(dbSession, corruptedEntities);
+ List<EntityDto> fixedEntities = dbClient.entityDao().selectByUuids(dbSession, corruptedEntities.stream().map(EntityDto::getUuid).toList());
+ fixedEntities.forEach(entity -> bulk.add(toDocument(entity).toIndexRequest()));
+ }
}
bulk.stop();
}
+ private void attemptToFixCorruptedEntities(DbSession dbSession, Set<EntityDto> corruptedEntities) {
+ for (EntityDto entity : corruptedEntities) {
+ dbClient.portfolioDao().selectByUuid(dbSession, entity.getUuid()).ifPresent(portfolio -> {
+ String portfolioUuid = portfolio.getUuid();
+ String rootUuid = portfolio.getRootUuid();
+ String parentUuid = portfolio.getParentUuid();
+ if (portfolioUuid.equals(rootUuid) && portfolioUuid.equals(parentUuid)) {
+ LOG.warn("Fixing corrupted portfolio tree for root portfolio {}", portfolioUuid);
+ portfolio.setParentUuid(null);
+ dbClient.portfolioDao().update(dbSession, portfolio);
+ } else {
+ LOG.warn("Detected portfolio tree corruption for portfolio {}", portfolioUuid);
+ }
+ });
+ }
+ }
+
private static void addProjectDeletionToBulkIndexer(BulkIndexer bulkIndexer, String projectUuid) {
SearchRequest searchRequest = EsClient.prepareSearch(TYPE_COMPONENT.getMainType())
.source(new SearchSourceBuilder().query(QueryBuilders.termQuery(ComponentIndexDefinition.FIELD_UUID, projectUuid)))
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java
index 343075669ef..90b6e0bfa01 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java
@@ -70,6 +70,7 @@ public class SearchRequest {
private Set<String> types;
private List<String> pciDss32;
private List<String> pciDss40;
+ private List<String> owaspMobileTop10For2024;
private List<String> owaspTop10;
private List<String> owaspAsvs40;
private List<String> owaspTop10For2021;
@@ -419,6 +420,16 @@ public class SearchRequest {
}
@CheckForNull
+ public List<String> getOwaspMobileTop10For2024() {
+ return owaspMobileTop10For2024;
+ }
+
+ public SearchRequest setOwaspMobileTop10For2024(@Nullable List<String> owaspMobileTop10For2024) {
+ this.owaspMobileTop10For2024 = owaspMobileTop10For2024;
+ return this;
+ }
+
+ @CheckForNull
public List<String> getOwaspTop10() {
return owaspTop10;
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java
index 972f06e0366..f40ddf46ccd 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java
@@ -77,8 +77,8 @@ public class TaintChecker {
}
private List<String> initializeRepositories() {
- List<String> repositories = new ArrayList<>(List.of("roslyn.sonaranalyzer.security.cs",
- "javasecurity", "jssecurity", "tssecurity", "phpsecurity", "pythonsecurity"));
+ List<String> repositories = new ArrayList<>(List.of("gosecurity", "javasecurity", "jssecurity", "kotlinsecurity", "phpsecurity", "pythonsecurity",
+ "roslyn.sonaranalyzer.security.cs", "tssecurity"));
if (!config.hasKey(EXTRA_TAINT_REPOSITORIES)) {
return repositories;
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java
index 4f044ecc2e0..e199187e443 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java
@@ -336,6 +336,16 @@ public class IssueDoc extends BaseDoc {
}
@CheckForNull
+ public Collection<String> getOwaspMobileTop10For2024() {
+ return getNullableField(IssueIndexDefinition.FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024);
+ }
+
+ public IssueDoc setOwaspMobileTop10For2024(@Nullable Collection<String> o) {
+ setField(IssueIndexDefinition.FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024, o);
+ return this;
+ }
+
+ @CheckForNull
public Collection<String> getOwaspTop10() {
return getNullableField(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10);
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java
index 685797b4db5..4f54d03cef1 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java
@@ -93,6 +93,7 @@ public class IssueIndexDefinition implements IndexDefinition {
public static final String FIELD_ISSUE_PCI_DSS_40 = "pciDss-4.0";
public static final String FIELD_ISSUE_OWASP_ASVS_40 = "owaspAsvs-4.0";
public static final String FIELD_ISSUE_OWASP_ASVS_40_LEVEL = "owaspAsvs-4.0-level";
+ public static final String FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024 = "owaspMobileTop10-2024";
public static final String FIELD_ISSUE_OWASP_TOP_10 = "owaspTop10";
public static final String FIELD_ISSUE_OWASP_TOP_10_2021 = "owaspTop10-2021";
public static final String FIELD_ISSUE_SANS_TOP_25 = "sansTop25";
@@ -180,6 +181,7 @@ public class IssueIndexDefinition implements IndexDefinition {
mapping.keywordFieldBuilder(FIELD_ISSUE_PCI_DSS_40).disableNorms().build();
mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_ASVS_40).disableNorms().build();
mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_ASVS_40_LEVEL).disableNorms().build();
+ mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024).disableNorms().build();
mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_TOP_10).disableNorms().build();
mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_TOP_10_2021).disableNorms().build();
mapping.keywordFieldBuilder(FIELD_ISSUE_SANS_TOP_25).disableNorms().build();
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java
index 676a6284b61..7383ef4a757 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java
@@ -32,8 +32,8 @@ import org.apache.ibatis.cursor.Cursor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.rules.CleanCodeAttribute;
-import org.sonar.core.rule.RuleType;
import org.sonar.api.server.rule.RulesDefinition.StigVersion;
+import org.sonar.core.rule.RuleType;
import org.sonar.db.DatabaseUtils;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
@@ -130,6 +130,7 @@ class IssueIteratorForSingleChunk implements IssueIterator {
doc.setImpacts(indexedIssueDto.getEffectiveImpacts());
SecurityStandards securityStandards = fromSecurityStandards(deserializeSecurityStandardsString(indexedIssueDto.getSecurityStandards()));
SecurityStandards.SQCategory sqCategory = securityStandards.getSqCategory();
+ doc.setOwaspMobileTop10For2024(securityStandards.getOwaspMobileTop10For2024());
doc.setOwaspTop10(securityStandards.getOwaspTop10());
doc.setOwaspTop10For2021(securityStandards.getOwaspTop10For2021());
doc.setStigAsdV5R3(securityStandards.getStig(StigVersion.ASD_V5R3));
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java
index 1c8dfb0696b..903fe411831 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java
@@ -20,6 +20,7 @@
package org.sonar.server.issue.index;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import javax.annotation.Nullable;
@@ -38,9 +39,11 @@ public class SecurityStandardCategoryStatistics {
private boolean hasMoreRules;
private final Optional<String> version;
private Optional<String> level = Optional.empty();
+ private final Map<String, Long> severityDistribution;
public SecurityStandardCategoryStatistics(String category, long vulnerabilities, OptionalInt vulnerabiliyRating, long toReviewSecurityHotspots,
- long reviewedSecurityHotspots, Integer securityReviewRating, @Nullable List<SecurityStandardCategoryStatistics> children, @Nullable String version) {
+ long reviewedSecurityHotspots, Integer securityReviewRating, @Nullable List<SecurityStandardCategoryStatistics> children, @Nullable String version,
+ Map<String, Long> severityDistribution) {
this.category = category;
this.vulnerabilities = vulnerabilities;
this.vulnerabilityRating = vulnerabiliyRating;
@@ -50,6 +53,30 @@ public class SecurityStandardCategoryStatistics {
this.children = children;
this.version = Optional.ofNullable(version);
this.hasMoreRules = false;
+ this.severityDistribution = severityDistribution;
+ }
+
+ public SecurityStandardCategoryStatistics withModifiedVulnerabilities(
+ int additionalVulnerabilities,
+ @Nullable Integer newVulnerabilityRating) {
+ OptionalInt newVulnerabilityRatingValue;
+
+ if (newVulnerabilityRating != null) {
+ newVulnerabilityRatingValue = OptionalInt.of(Math.max(newVulnerabilityRating, this.getVulnerabilityRating().orElse(0)));
+ } else {
+ newVulnerabilityRatingValue = this.getVulnerabilityRating();
+ }
+
+ return new SecurityStandardCategoryStatistics(
+ this.getCategory(),
+ this.getVulnerabilities() + additionalVulnerabilities,
+ newVulnerabilityRatingValue,
+ this.getToReviewSecurityHotspots(),
+ this.getReviewedSecurityHotspots(),
+ this.getSecurityReviewRating(),
+ this.getChildren(),
+ this.getVersion().orElse(null),
+ this.getSeverityDistribution());
}
public String getCategory() {
@@ -118,4 +145,8 @@ public class SecurityStandardCategoryStatistics {
public void setHasMoreRules(boolean hasMoreRules) {
this.hasMoreRules = hasMoreRules;
}
+
+ public Map<String, Long> getSeverityDistribution() {
+ return severityDistribution;
+ }
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java
index 154d5db7640..352e22c2995 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java
@@ -44,7 +44,7 @@ import static org.sonar.server.notification.NotificationManager.SubscriberPermis
public class ChangesOnMyIssueNotificationHandler extends EmailNotificationHandler<IssuesChangesNotification> {
- private static final String KEY = "ChangesOnMyIssue";
+ public static final String KEY = "ChangesOnMyIssue";
private static final NotificationDispatcherMetadata METADATA = NotificationDispatcherMetadata.create(KEY)
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java
index 389d1697c5f..3da617a1be9 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java
@@ -26,6 +26,7 @@ import org.sonar.api.server.ServerSide;
import org.sonar.issue.workflow.statemachine.StateMachine;
import org.sonar.issue.workflow.statemachine.Transition;
+import static java.util.function.Predicate.not;
import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
import static org.sonar.api.issue.Issue.RESOLUTION_REMOVED;
@@ -56,6 +57,9 @@ public class CodeQualityIssueWorkflowDefinition {
private static final Consumer<CodeQualityIssueWorkflowActions> UNSET_RESOLUTION = CodeQualityIssueWorkflowActions::unsetResolution;
private static final Consumer<CodeQualityIssueWorkflowActions> RESTORE_RESOLUTION = CodeQualityIssueWorkflowActions::restoreResolution;
+ private static final Predicate<CodeQualityIssueWorkflowEntity> AUTOMATIC_REOPEN_PREDICATE = not(CodeQualityIssueWorkflowEntity::isBeingClosed)
+ .and(issue -> issue.hasAnyResolution(RESOLUTION_FIXED));
+
private final StateMachine<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions> machine;
public CodeQualityIssueWorkflowDefinition() {
@@ -181,9 +185,7 @@ public class CodeQualityIssueWorkflowDefinition {
// Reopen issues that are marked as resolved but that are still alive.
.transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder("automaticreopen")
.from(STATUS_RESOLVED).to(STATUS_REOPENED)
- .conditions(
- Predicate.not(CodeQualityIssueWorkflowEntity::isBeingClosed),
- i -> i.hasAnyResolution(RESOLUTION_FIXED))
+ .conditions(AUTOMATIC_REOPEN_PREDICATE)
.actions(UNSET_RESOLUTION, UNSET_CLOSE_DATE)
.automatic()
.build())
@@ -227,6 +229,8 @@ public class CodeQualityIssueWorkflowDefinition {
.from(STATUS_RESOLVED)
.to(STATUS_REOPENED)
.conditions(
+ // We check this first condition to avoid overlap with the automaticreopen transition.
+ not(AUTOMATIC_REOPEN_PREDICATE),
CodeQualityIssueWorkflowEntity::isTaintVulnerability,
CodeQualityIssueWorkflowEntity::locationsChanged)
.actions(
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowTransition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowTransition.java
index af6c9dae152..659774ab983 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowTransition.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/securityhotspot/SecurityHotspotWorkflowTransition.java
@@ -26,18 +26,7 @@ import java.util.stream.Stream;
import org.sonar.server.issue.workflow.WorkflowTransition;
public enum SecurityHotspotWorkflowTransition implements WorkflowTransition {
- /**
- * @deprecated since 8.1, transition has no effect
- */
- @Deprecated
- SET_AS_IN_REVIEW("setinreview"),
- /**
- * @since 7.8
- * @deprecated since 8.1, security hotspots can no longer be converted to vulnerabilities
- */
- @Deprecated
- OPEN_AS_VULNERABILITY("openasvulnerability"),
RESOLVE_AS_REVIEWED("resolveasreviewed"),
RESOLVE_AS_SAFE("resolveassafe"),
RESOLVE_AS_ACKNOWLEDGED("resolveasacknowledged"),
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java b/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java
index eaead555007..9e4b5df7878 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java
@@ -65,6 +65,7 @@ public final class SecurityStandards {
@Deprecated(since = "10.0", forRemoval = true)
public static final String SANS_TOP_25_POROUS_DEFENSES = "porous-defenses";
+ private static final String OWASP_MOBILE_TOP10_2024_PREFIX = "owaspMobileTop10-2024:";
private static final String OWASP_TOP10_PREFIX = "owaspTop10:";
private static final String OWASP_TOP10_2021_PREFIX = "owaspTop10-2021:";
private static final String PCI_DSS_32_PREFIX = V3_2.prefix() + ":";
@@ -104,14 +105,20 @@ public final class SecurityStandards {
public static final List<String> CWE_TOP25_2023 = List.of("787", "79", "89", "416", "78", "20", "125", "22", "352", "434", "862", "476", "287", "190", "502",
"77", "119", "798", "918", "306", "362", "269", "94", "863", "276");
+ // https://cwe.mitre.org/top25/archive/2024/2024_cwe_top25.html#tableView
+ public static final List<String> CWE_TOP25_2024 = List.of("79", "787", "89", "352", "22", "125", "78", "416", "862", "434", "94", "20", "77", "287", "269",
+ "502", "200", "863", "918", "119", "476", "798", "190", "400", "306");
+
public static final String CWE_YEAR_2021 = "2021";
public static final String CWE_YEAR_2022 = "2022";
public static final String CWE_YEAR_2023 = "2023";
+ public static final String CWE_YEAR_2024 = "2024";
public static final Map<String, List<String>> CWES_BY_CWE_TOP_25 = Map.of(
CWE_YEAR_2021, CWE_TOP25_2021,
CWE_YEAR_2022, CWE_TOP25_2022,
- CWE_YEAR_2023, CWE_TOP25_2023);
+ CWE_YEAR_2023, CWE_TOP25_2023,
+ CWE_YEAR_2024, CWE_TOP25_2024);
private static final List<String> OWASP_ASVS_40_LEVEL_1 = List.of("2.1.1", "2.1.10", "2.1.11", "2.1.12", "2.1.2", "2.1.3", "2.1.4", "2.1.5", "2.1.6", "2.1.7", "2.1.8", "2.1.9",
"2.10.1", "2.10.2", "2.10.3", "2.10.4", "2.2.1", "2.2.2", "2.2.3", "2.3.1", "2.5.1", "2.5.2", "2.5.3", "2.5.4", "2.5.5", "2.5.6", "2.7.1", "2.7.2", "2.7.3", "2.7.4", "2.8.1",
@@ -447,6 +454,10 @@ public final class SecurityStandards {
return getMatchingStandards(standards, OWASP_ASVS_40_PREFIX);
}
+ public Set<String> getOwaspMobileTop10For2024() {
+ return getMatchingStandards(standards, OWASP_MOBILE_TOP10_2024_PREFIX);
+ }
+
public Set<String> getOwaspTop10() {
return getMatchingStandards(standards, OWASP_TOP10_PREFIX);
}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java
index 8e5073080ad..4cbf7aeeb41 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java
@@ -44,13 +44,15 @@ public class TaintCheckerTest {
public void test_getTaintIssuesOnly() {
List<IssueDto> taintIssues = underTest.getTaintIssuesOnly(getIssues());
- assertThat(taintIssues).hasSize(6);
+ assertThat(taintIssues).hasSize(8);
assertThat(taintIssues.get(0).getKey()).isEqualTo("taintIssue1");
assertThat(taintIssues.get(1).getKey()).isEqualTo("taintIssue2");
assertThat(taintIssues.get(2).getKey()).isEqualTo("taintIssue3");
assertThat(taintIssues.get(3).getKey()).isEqualTo("taintIssue4");
assertThat(taintIssues.get(4).getKey()).isEqualTo("taintIssue5");
assertThat(taintIssues.get(5).getKey()).isEqualTo("taintIssue6");
+ assertThat(taintIssues.get(6).getKey()).isEqualTo("taintIssue7");
+ assertThat(taintIssues.get(7).getKey()).isEqualTo("taintIssue8");
}
@Test
@@ -69,7 +71,7 @@ public class TaintCheckerTest {
Map<Boolean, List<IssueDto>> issuesByTaintStatus = underTest.mapIssuesByTaintStatus(getIssues());
assertThat(issuesByTaintStatus.keySet()).hasSize(2);
- assertThat(issuesByTaintStatus.get(true)).hasSize(6);
+ assertThat(issuesByTaintStatus.get(true)).hasSize(8);
assertThat(issuesByTaintStatus.get(false)).hasSize(3);
assertThat(issuesByTaintStatus.get(true).get(0).getKey()).isEqualTo("taintIssue1");
@@ -78,6 +80,8 @@ public class TaintCheckerTest {
assertThat(issuesByTaintStatus.get(true).get(3).getKey()).isEqualTo("taintIssue4");
assertThat(issuesByTaintStatus.get(true).get(4).getKey()).isEqualTo("taintIssue5");
assertThat(issuesByTaintStatus.get(true).get(5).getKey()).isEqualTo("taintIssue6");
+ assertThat(issuesByTaintStatus.get(true).get(6).getKey()).isEqualTo("taintIssue7");
+ assertThat(issuesByTaintStatus.get(true).get(7).getKey()).isEqualTo("taintIssue8");
assertThat(issuesByTaintStatus.get(false).get(0).getKey()).isEqualTo("standardIssue1");
assertThat(issuesByTaintStatus.get(false).get(1).getKey()).isEqualTo("standardIssue2");
@@ -87,9 +91,9 @@ public class TaintCheckerTest {
@Test
public void test_getTaintRepositories() {
assertThat(underTest.getTaintRepositories())
- .hasSize(6)
- .containsExactlyInAnyOrder("roslyn.sonaranalyzer.security.cs", "javasecurity", "jssecurity",
- "tssecurity", "phpsecurity", "pythonsecurity");
+ .hasSize(8)
+ .containsExactlyInAnyOrder("gosecurity", "javasecurity", "jssecurity", "kotlinsecurity", "phpsecurity", "pythonsecurity",
+ "roslyn.sonaranalyzer.security.cs", "tssecurity");
}
@Test
@@ -98,9 +102,9 @@ public class TaintCheckerTest {
when(configuration.getStringArray(EXTRA_TAINT_REPOSITORIES)).thenReturn(new String[]{"extra-1", "extra-2"});
TaintChecker underTest = new TaintChecker(configuration);
assertThat(underTest.getTaintRepositories())
- .hasSize(8)
- .containsExactlyInAnyOrder("roslyn.sonaranalyzer.security.cs", "javasecurity", "jssecurity",
- "tssecurity", "phpsecurity", "pythonsecurity", "extra-1", "extra-2");
+ .hasSize(10)
+ .containsExactlyInAnyOrder("gosecurity", "javasecurity", "jssecurity", "kotlinsecurity", "phpsecurity", "pythonsecurity",
+ "roslyn.sonaranalyzer.security.cs", "tssecurity", "extra-1", "extra-2");
}
@Test
@@ -135,6 +139,8 @@ public class TaintCheckerTest {
issues.add(createIssueWithRepository("taintIssue4", "tssecurity"));
issues.add(createIssueWithRepository("taintIssue5", "phpsecurity"));
issues.add(createIssueWithRepository("taintIssue6", "pythonsecurity"));
+ issues.add(createIssueWithRepository("taintIssue7", "kotlinsecurity"));
+ issues.add(createIssueWithRepository("taintIssue8", "gosecurity"));
issues.add(createIssueWithRepository("standardIssue1", "java"));
issues.add(createIssueWithRepository("standardIssue2", "python"));
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java
index 13c8dd28285..a23c45cd248 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java
@@ -20,6 +20,9 @@
package org.sonar.server.issue.index;
import java.util.ArrayList;
+import java.util.Map;
+import java.util.OptionalInt;
+
import org.junit.Test;
import static java.util.OptionalInt.empty;
@@ -31,7 +34,7 @@ public class SecurityStandardCategoryStatisticsTest {
public void hasMoreRules_default_false() {
SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
"cat", 0, empty(), 0,
- 0, 5, null, null
+ 0, 5, null, null, Map.of()
);
assertThat(standardCategoryStatistics.hasMoreRules()).isFalse();
}
@@ -40,7 +43,7 @@ public class SecurityStandardCategoryStatisticsTest {
public void hasMoreRules_is_updatable() {
SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
"cat", 0, empty(), 0,
- 0, 5, null, null
+ 0, 5, null, null, Map.of()
);
standardCategoryStatistics.setHasMoreRules(true);
assertThat(standardCategoryStatistics.hasMoreRules()).isTrue();
@@ -50,7 +53,7 @@ public class SecurityStandardCategoryStatisticsTest {
public void test_getters() {
SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
"cat", 1, empty(), 0,
- 0, 5, new ArrayList<>(), "version"
+ 0, 5, new ArrayList<>(), "version", Map.of()
).setLevel("1");
standardCategoryStatistics.setActiveRules(3);
@@ -69,6 +72,47 @@ public class SecurityStandardCategoryStatisticsTest {
assertThat(standardCategoryStatistics.getVersion().get()).contains("version");
assertThat(standardCategoryStatistics.getLevel().get()).contains("1");
assertThat(standardCategoryStatistics.hasMoreRules()).isFalse();
+ assertThat(standardCategoryStatistics.getSeverityDistribution()).isEmpty();
+ }
+
+ @Test
+ public void withModifiedVulnerabilities() {
+ SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
+ "cat", 1, empty(), 0,
+ 0, 5, null, null, Map.of()
+ );
+
+ SecurityStandardCategoryStatistics modified = standardCategoryStatistics.withModifiedVulnerabilities(2, 3);
+
+ assertThat(modified.getVulnerabilities()).isEqualTo(3);
+ assertThat(modified.getVulnerabilityRating()).isPresent();
+ assertThat(modified.getVulnerabilityRating().getAsInt()).isEqualTo(3);
+ }
+
+ @Test
+ public void withModifiedVulnerabilities_noNewRating() {
+ SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
+ "cat", 1, OptionalInt.of(1), 0,
+ 0, 5, null, null, Map.of()
+ );
+
+ SecurityStandardCategoryStatistics modified = standardCategoryStatistics.withModifiedVulnerabilities(2, null);
+
+ assertThat(modified.getVulnerabilities()).isEqualTo(3);
+ assertThat(modified.getVulnerabilityRating()).isPresent();
+ assertThat(modified.getVulnerabilityRating().getAsInt()).isEqualTo(1);
}
+ @Test
+ public void withModifiedVulnerabilities_usesLowestRating() {
+ SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics(
+ "cat", 1, OptionalInt.of(5), 0,
+ 0, 5, null, null, Map.of()
+ );
+
+ SecurityStandardCategoryStatistics modified = standardCategoryStatistics.withModifiedVulnerabilities(2, 3);
+
+ assertThat(modified.getVulnerabilityRating()).isPresent();
+ assertThat(modified.getVulnerabilityRating().getAsInt()).isEqualTo(5);
+ }
}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java
index 386b3666c26..956ef000a0e 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java
@@ -60,6 +60,7 @@ import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.api.issue.Issue.STATUS_REOPENED;
import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
import static org.sonar.core.issue.IssueChangeContext.issueChangeContextByScanBuilder;
+import static org.sonar.core.rule.RuleType.VULNERABILITY;
class IssueWorkflowForCodeQualityIssuesTest {
@@ -455,6 +456,27 @@ class IssueWorkflowForCodeQualityIssuesTest {
assertThat(issue.assignee()).isNull();
}
+ @Test
+ void doManualTransition_shouldUseAutomaticReopenTransitionOnTaintVulnerability_whenMarkedAsResolvedButStillAlive() {
+ DefaultIssue issue = new DefaultIssue()
+ .setKey("issue_key")
+ .setRuleKey(RuleKey.of("xoo", "S001"))
+ .setStatus(STATUS_RESOLVED)
+ .setResolution(RESOLUTION_FIXED)
+ .setLocationsChanged(true)
+ .setNew(false)
+ .setType(VULNERABILITY)
+ .setBeingClosed(false);
+ when(taintChecker.isTaintVulnerability(issue))
+ .thenReturn(true);
+
+ underTest.doAutomaticTransition(issue, issueChangeContextByScanBuilder(new Date()).build());
+
+ assertThat(issue.issueStatus()).isEqualTo(IssueStatus.OPEN);
+ List<DefaultIssueComment> issueComments = issue.defaultIssueComments();
+ assertThat(issueComments).isEmpty();
+ }
+
private static DefaultIssue newClosedIssue(String resolution) {
return new DefaultIssue()
.setKey("ABCDE")
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java
index c02feb8bb7e..543ac842925 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java
@@ -63,6 +63,14 @@ class SecurityStandardsTest {
}
@Test
+ void fromSecurityStandards_from_empty_set_has_no_OwaspMobileTop10_standard() {
+ SecurityStandards securityStandards = fromSecurityStandards(emptySet());
+
+ assertThat(securityStandards.getStandards()).isEmpty();
+ assertThat(securityStandards.getOwaspMobileTop10For2024()).isEmpty();
+ }
+
+ @Test
void fromSecurityStandards_from_empty_set_has_no_OwaspTop10_standard() {
SecurityStandards securityStandards = fromSecurityStandards(emptySet());
diff --git a/server/sonar-telemetry/src/main/java/org/sonar/telemetry/metrics/TelemetryMetricsMapper.java b/server/sonar-telemetry/src/main/java/org/sonar/telemetry/metrics/TelemetryMetricsMapper.java
index 6630ab35a9c..c5dfe9fbcad 100644
--- a/server/sonar-telemetry/src/main/java/org/sonar/telemetry/metrics/TelemetryMetricsMapper.java
+++ b/server/sonar-telemetry/src/main/java/org/sonar/telemetry/metrics/TelemetryMetricsMapper.java
@@ -20,7 +20,6 @@
package org.sonar.telemetry.metrics;
import java.util.Collections;
-import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.sonar.telemetry.core.Granularity;
@@ -40,43 +39,57 @@ public class TelemetryMetricsMapper {
switch (provider.getDimension()) {
case INSTALLATION -> {
return mapInstallationMetric(provider);
- } case PROJECT -> {
+ }
+ case PROJECT -> {
return mapProjectMetric(provider);
- } case USER -> {
+ }
+ case USER -> {
return mapUserMetric(provider);
- } case LANGUAGE -> {
+ }
+ case LANGUAGE -> {
return mapLanguageMetric(provider);
- } default -> throw new IllegalArgumentException("Dimension: " + provider.getDimension() + " not yet implemented.");
+ }
+ default -> throw new IllegalArgumentException("Dimension: " + provider.getDimension() + " not yet implemented.");
}
}
private static Set<Metric> mapInstallationMetric(TelemetryDataProvider<?> provider) {
- Optional<?> optionalValue = provider.getValue();
-
- Granularity granularity = provider.getGranularity();
-
- if (granularity == Granularity.ADHOC && !provider.getValues().isEmpty()) {
- return provider.getValues().entrySet().stream()
+ // Case 1: the provider has implemented getValues() and it is non‐empty
+ var multiValues = provider.getValues();
+ if (!multiValues.isEmpty()) {
+ return multiValues.entrySet().stream()
.map(entry -> new InstallationMetric(
provider.getMetricKey() + "." + entry.getKey(),
entry.getValue(),
provider.getType(),
- granularity
- )).collect(Collectors.toSet());
+ provider.getGranularity()))
+ .collect(Collectors.toSet());
}
- if (granularity == Granularity.ADHOC && optionalValue.isEmpty()) {
- return Collections.emptySet();
+ // Case 2: the provider has implemented getValue() and it is non‐empty
+ var singleValue = provider.getValue();
+ if (singleValue.isPresent()) {
+ return Collections.singleton(new InstallationMetric(
+ provider.getMetricKey(),
+ singleValue.orElse(null),
+ provider.getType(),
+ provider.getGranularity()));
}
- return Collections.singleton(new InstallationMetric(
- provider.getMetricKey(),
- optionalValue.orElse(null),
- provider.getType(),
- granularity
- ));
+ // Case 3: the provider has not implemented getValue() or getValues(), or both are empty
+ if (provider.getGranularity() == Granularity.ADHOC) {
+ return Collections.emptySet();
+ } else {
+ // It's not clear whether we actually want to send a null metric in this case, but we do for now to be consistent with the previous implementation.
+ return Collections.singleton(new InstallationMetric(
+ provider.getMetricKey(),
+ null,
+ provider.getType(),
+ provider.getGranularity()));
+ }
}
+ // Note: Dimension.USER does not currently support getValue(). But we just silently ignore it if a provider tries to use it.
private static Set<Metric> mapUserMetric(TelemetryDataProvider<?> provider) {
return provider.getValues().entrySet().stream()
.map(entry -> new UserMetric(
@@ -84,10 +97,11 @@ public class TelemetryMetricsMapper {
entry.getValue(),
entry.getKey(),
provider.getType(),
- provider.getGranularity()
- )).collect(Collectors.toSet());
+ provider.getGranularity()))
+ .collect(Collectors.toSet());
}
+ // Note: Dimension.PROJECT does not currently support getValue(). But we just silently ignore it if a provider tries to use it.
private static Set<Metric> mapProjectMetric(TelemetryDataProvider<?> provider) {
return provider.getValues().entrySet().stream()
.map(entry -> new ProjectMetric(
@@ -95,10 +109,11 @@ public class TelemetryMetricsMapper {
entry.getValue(),
entry.getKey(),
provider.getType(),
- provider.getGranularity()
- )).collect(Collectors.toSet());
+ provider.getGranularity()))
+ .collect(Collectors.toSet());
}
+ // Note: Dimension.LANGUAGE does not currently support getValue(). But we just silently ignore it if a provider tries to use it.
private static Set<Metric> mapLanguageMetric(TelemetryDataProvider<?> provider) {
return provider.getValues().entrySet().stream()
.map(entry -> new LanguageMetric(
@@ -106,8 +121,7 @@ public class TelemetryMetricsMapper {
entry.getValue(),
entry.getKey(),
provider.getType(),
- provider.getGranularity()
- )).collect(Collectors.toSet());
+ provider.getGranularity()))
+ .collect(Collectors.toSet());
}
-
}
diff --git a/server/sonar-telemetry/src/test/java/org/sonar/telemetry/metrics/TelemetryMetricsMapperTest.java b/server/sonar-telemetry/src/test/java/org/sonar/telemetry/metrics/TelemetryMetricsMapperTest.java
index 69870bf024f..193566b5f68 100644
--- a/server/sonar-telemetry/src/test/java/org/sonar/telemetry/metrics/TelemetryMetricsMapperTest.java
+++ b/server/sonar-telemetry/src/test/java/org/sonar/telemetry/metrics/TelemetryMetricsMapperTest.java
@@ -21,6 +21,8 @@ package org.sonar.telemetry.metrics;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import org.assertj.core.groups.Tuple;
import org.junit.jupiter.api.Test;
@@ -40,8 +42,14 @@ import static org.assertj.core.api.Assertions.tuple;
class TelemetryMetricsMapperTest {
@Test
- void mapFromDataProvider_whenInstallationProvider() {
- TelemetryDataProvider<String> provider = new TestTelemetryBean(Dimension.INSTALLATION);
+ void mapFromDataProvider_withInstallationProviderSingleValue_returnsSingleValue() {
+ // Override multi-value method to return empty. Keep the single-value method defined in TestTelemetryBean.
+ TelemetryDataProvider<String> provider = new TestTelemetryBean(Dimension.INSTALLATION) {
+ @Override
+ public Map<String, String> getValues() {
+ return Map.of();
+ }
+ };
Set<Metric> metrics = TelemetryMetricsMapper.mapFromDataProvider(provider);
List<InstallationMetric> userMetrics = retrieveList(metrics);
@@ -49,17 +57,70 @@ class TelemetryMetricsMapperTest {
assertThat(userMetrics)
.extracting(InstallationMetric::getKey, InstallationMetric::getType, InstallationMetric::getValue, InstallationMetric::getGranularity)
.containsExactlyInAnyOrder(
- tuple("telemetry-bean-a", TelemetryDataType.STRING, "value", Granularity.DAILY)
- );
+ tuple("telemetry-bean-a", TelemetryDataType.STRING, "value", Granularity.DAILY));
}
@Test
- void mapFromDataProvider_whenInstallationProviderWithMultiValue() {
+ void mapFromDataProvider_withInstallationProviderMultiValues_returnsMultipleValues() {
+ // Override single-value method to return empty. Keep the multi-value method defined in TestTelemetryBean.
+ TelemetryDataProvider<String> provider = new TestTelemetryBean(Dimension.INSTALLATION) {
+ @Override
+ public Optional<String> getValue() {
+ return Optional.empty();
+ }
+ };
+
+ Set<Metric> metrics = TelemetryMetricsMapper.mapFromDataProvider(provider);
+ List<InstallationMetric> userMetrics = retrieveList(metrics);
+
+ assertThat(userMetrics)
+ .extracting(InstallationMetric::getKey, InstallationMetric::getType, InstallationMetric::getValue, InstallationMetric::getGranularity)
+ .containsExactlyInAnyOrder(
+ tuple("telemetry-bean-a.key-1", TelemetryDataType.STRING, "value-1", Granularity.DAILY),
+ tuple("telemetry-bean-a.key-2", TelemetryDataType.STRING, "value-2", Granularity.DAILY));
+ }
+
+ @Test
+ void mapFromDataProvider_withInstallationProviderAdhocNoValues_returnEmptySet() {
+ // Override single-value and multi-value methods to return empty.
TelemetryDataProvider<String> provider = new TestTelemetryBean(Dimension.INSTALLATION) {
@Override
public Granularity getGranularity() {
return Granularity.ADHOC;
}
+
+ @Override
+ public Optional<String> getValue() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Map<String, String> getValues() {
+ return Map.of();
+ }
+ };
+
+ Set<Metric> metrics = TelemetryMetricsMapper.mapFromDataProvider(provider);
+ List<InstallationMetric> userMetrics = retrieveList(metrics);
+
+ assertThat(userMetrics)
+ .extracting(InstallationMetric::getKey, InstallationMetric::getType, InstallationMetric::getValue, InstallationMetric::getGranularity)
+ .isEmpty();
+ }
+
+ @Test
+ void mapFromDataProvider_withInstallationProviderDailyNoValues_returnTelemetryWithNullValue() {
+ // Override single-value and multi-value methods to return empty.
+ TelemetryDataProvider<String> provider = new TestTelemetryBean(Dimension.INSTALLATION) {
+ @Override
+ public Optional<String> getValue() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Map<String, String> getValues() {
+ return Map.of();
+ }
};
Set<Metric> metrics = TelemetryMetricsMapper.mapFromDataProvider(provider);
@@ -68,9 +129,7 @@ class TelemetryMetricsMapperTest {
assertThat(userMetrics)
.extracting(InstallationMetric::getKey, InstallationMetric::getType, InstallationMetric::getValue, InstallationMetric::getGranularity)
.containsExactlyInAnyOrder(
- tuple("telemetry-bean-a.key-1", TelemetryDataType.STRING, "value-1", Granularity.ADHOC),
- tuple("telemetry-bean-a.key-2", TelemetryDataType.STRING, "value-2", Granularity.ADHOC)
- );
+ tuple("telemetry-bean-a", TelemetryDataType.STRING, null, Granularity.DAILY));
}
@Test
@@ -83,8 +142,7 @@ class TelemetryMetricsMapperTest {
assertThat(list)
.extracting(UserMetric::getKey, UserMetric::getType, UserMetric::getUserUuid, UserMetric::getValue, UserMetric::getGranularity)
.containsExactlyInAnyOrder(
- expected()
- );
+ expected());
}
@Test
@@ -97,8 +155,7 @@ class TelemetryMetricsMapperTest {
assertThat(list)
.extracting(LanguageMetric::getKey, LanguageMetric::getType, LanguageMetric::getLanguage, LanguageMetric::getValue, LanguageMetric::getGranularity)
.containsExactlyInAnyOrder(
- expected()
- );
+ expected());
}
@Test
@@ -111,8 +168,7 @@ class TelemetryMetricsMapperTest {
assertThat(list)
.extracting(ProjectMetric::getKey, ProjectMetric::getType, ProjectMetric::getProjectUuid, ProjectMetric::getValue, ProjectMetric::getGranularity)
.containsExactlyInAnyOrder(
- expected()
- );
+ expected());
}
@Test
@@ -135,16 +191,14 @@ class TelemetryMetricsMapperTest {
assertThat(userMetrics)
.extracting(InstallationMetric::getKey, InstallationMetric::getType, InstallationMetric::getValue, InstallationMetric::getGranularity)
.containsExactlyInAnyOrder(
- tuple("telemetry-adhoc-bean", TelemetryDataType.BOOLEAN, true, Granularity.ADHOC)
- );
+ tuple("telemetry-adhoc-bean", TelemetryDataType.BOOLEAN, true, Granularity.ADHOC));
}
private static Tuple[] expected() {
- return new Tuple[]
- {
- tuple("telemetry-bean-a", TelemetryDataType.STRING, "key-1", "value-1", Granularity.DAILY),
- tuple("telemetry-bean-a", TelemetryDataType.STRING, "key-2", "value-2", Granularity.DAILY)
- };
+ return new Tuple[] {
+ tuple("telemetry-bean-a", TelemetryDataType.STRING, "key-1", "value-1", Granularity.DAILY),
+ tuple("telemetry-bean-a", TelemetryDataType.STRING, "key-2", "value-2", Granularity.DAILY)
+ };
}
private static <T extends Metric> List<T> retrieveList(Set<Metric> metrics) {
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/TooManyRequestsException.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/TooManyRequestsException.java
new file mode 100644
index 00000000000..c54e898ff16
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/TooManyRequestsException.java
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.exceptions;
+
+public class TooManyRequestsException extends RuntimeException {
+
+ public TooManyRequestsException(String message) {
+ super(message);
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java
index ad6f305c361..89a712b5a53 100644
--- a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java
@@ -32,4 +32,11 @@ public interface QGChangeEventListeners {
*/
void broadcastOnIssueChange(List<DefaultIssue> changedIssues, Collection<QGChangeEvent> qgChangeEvents, boolean fromAlm);
+ /**
+ * Broadcast events regardless of any changed file analysis issues. Used when non-file analysis tools (ex: SCA)
+ * need to send events.
+ *
+ * @param fromAlm: true if issues changes were initiated by an ALM.
+ */
+ void broadcastOnAnyChange(Collection<QGChangeEvent> qgChangeEvents, boolean fromAlm);
}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
index 65eb2433cbd..2068279f93f 100644
--- a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
@@ -62,6 +62,21 @@ public class QGChangeEventListenersImpl implements QGChangeEventListeners {
}
}
+ @Override
+ public void broadcastOnAnyChange(Collection<QGChangeEvent> changeEvents, boolean fromAlm) {
+ if (listeners.isEmpty() || changeEvents.isEmpty()) {
+ return;
+ }
+
+ try {
+ for (var changeEvent : changeEvents) {
+ listeners.forEach(listener -> broadcastChangeEventToListener(Set.of(), changeEvent, listener));
+ }
+ } catch (Error e) {
+ LOG.warn(format("Broadcasting to listeners failed for %s events", changeEvents.size()), e);
+ }
+ }
+
private void broadcastChangeEventsToBranches(List<DefaultIssue> issues, Collection<QGChangeEvent> changeEvents, boolean fromAlm) {
Multimap<String, QGChangeEvent> eventsByBranchUuid = changeEvents.stream()
.collect(MoreCollectors.index(qgChangeEvent -> qgChangeEvent.getBranch().getUuid()));
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java
index 4303a88bfe9..9a056ad8fdb 100644
--- a/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java
@@ -238,6 +238,34 @@ public class QGChangeEventListenersImplTest {
}
@Test
+ public void broadcastOnAnyChange_whenNoListeners_thenDoesNothing() {
+ var qgChangeEventListeners = new QGChangeEventListenersImpl(Set.of());
+
+ qgChangeEventListeners.broadcastOnAnyChange(singletonList(component1QGChangeEvent), false);
+
+ verifyNoInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ public void broadcastOnAnyChange_whenNoChangeEvents_thenDoesNothing() {
+ underTest.broadcastOnAnyChange(List.of(), false);
+
+ verifyNoInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ public void broadcastOnAnyChange_whenListenersAndChangeEvents_thenBroadcastsToEachListener() {
+ underTest.broadcastOnAnyChange(singletonList(component1QGChangeEvent), false);
+
+ ArgumentCaptor<Set<ChangedIssue>> changedIssuesCaptor = newSetCaptor();
+ inOrder.verify(listener1).onIssueChanges(same(component1QGChangeEvent), changedIssuesCaptor.capture());
+ Set<ChangedIssue> changedIssues = changedIssuesCaptor.getValue();
+ inOrder.verify(listener2).onIssueChanges(same(component1QGChangeEvent), same(changedIssues));
+ inOrder.verify(listener3).onIssueChanges(same(component1QGChangeEvent), same(changedIssues));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
public void isNotClosed_returns_true_if_issue_in_one_of_opened_states() {
DefaultIssue defaultIssue = new DefaultIssue();
defaultIssue.setStatus(Issue.STATUS_REOPENED);
diff --git a/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/JwtHttpHandlerIT.java b/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/JwtHttpHandlerIT.java
index d280a8f2384..729cafe8422 100644
--- a/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/JwtHttpHandlerIT.java
+++ b/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/JwtHttpHandlerIT.java
@@ -23,14 +23,18 @@ import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ClaimsBuilder;
import io.jsonwebtoken.impl.DefaultClaimsBuilder;
import jakarta.servlet.http.HttpSession;
+import java.time.Duration;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
+import org.sonar.api.config.Configuration;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.server.http.Cookie;
import org.sonar.api.server.http.HttpRequest;
@@ -44,8 +48,9 @@ import org.sonar.db.user.UserDto;
import org.sonar.server.http.JakartaHttpRequest;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.entry;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -60,7 +65,7 @@ import static org.mockito.Mockito.when;
import static org.sonar.db.user.UserTesting.newUserDto;
import static org.sonar.server.authentication.Cookies.SET_COOKIE;
-public class JwtHttpHandlerIT {
+class JwtHttpHandlerIT {
private static final String JWT_TOKEN = "TOKEN";
private static final String CSRF_STATE = "CSRF_STATE";
@@ -72,7 +77,7 @@ public class JwtHttpHandlerIT {
private static final long IN_FIVE_MINUTES = NOW + 5 * 60 * 1000L;
- @Rule
+ @RegisterExtension
public DbTester db = DbTester.create();
private final DbClient dbClient = db.getDbClient();
@@ -86,18 +91,21 @@ public class JwtHttpHandlerIT {
private final MapSettings settings = new MapSettings();
private final JwtSerializer jwtSerializer = mock(JwtSerializer.class);
private final JwtCsrfVerifier jwtCsrfVerifier = mock(JwtCsrfVerifier.class);
+ private final ActiveTimeoutProvider activeTimeoutProvider = mock(ActiveTimeoutProvider.class);
- private JwtHttpHandler underTest = new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier);
+ private JwtHttpHandler underTest;
- @Before
- public void setUp() {
+ @BeforeEach
+ void setUp() {
when(system2.now()).thenReturn(NOW);
when(jwtSerializer.encode(any(JwtSerializer.JwtSession.class))).thenReturn(JWT_TOKEN);
when(jwtCsrfVerifier.generateState(eq(request), eq(response), anyInt())).thenReturn(CSRF_STATE);
+ when(activeTimeoutProvider.getActiveSessionTimeout()).thenReturn(Duration.ofDays(90));
+ underTest = new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier, activeTimeoutProvider);
}
@Test
- public void create_token() {
+ void create_token() {
UserDto user = db.users().insertUser();
underTest.generateToken(user, request, response);
@@ -109,7 +117,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void generate_csrf_state_when_creating_token() {
+ void generate_csrf_state_when_creating_token() {
UserDto user = db.users().insertUser();
underTest.generateToken(user, request, response);
@@ -121,12 +129,13 @@ public class JwtHttpHandlerIT {
}
@Test
- public void generate_token_is_using_session_timeout_from_settings() {
+ void generate_token_is_using_inactive_session_timeout_from_settings() {
UserDto user = db.users().insertUser();
int sessionTimeoutInMinutes = 10;
+ Configuration config = settings.asConfig();
settings.setProperty("sonar.web.sessionTimeoutInMinutes", sessionTimeoutInMinutes);
- underTest = new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier);
+ underTest = new JwtHttpHandler(system2, dbClient, config, jwtSerializer, jwtCsrfVerifier, activeTimeoutProvider);
underTest.generateToken(user, request, response);
verify(jwtSerializer).encode(jwtArgumentCaptor.capture());
@@ -134,12 +143,12 @@ public class JwtHttpHandlerIT {
}
@Test
- public void session_timeout_property_cannot_be_updated() {
+ void inactive_session_timeout_property_cannot_be_updated() {
UserDto user = db.users().insertUser();
int firstSessionTimeoutInMinutes = 10;
settings.setProperty("sonar.web.sessionTimeoutInMinutes", firstSessionTimeoutInMinutes);
- underTest = new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier);
+ underTest = new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier, activeTimeoutProvider);
underTest.generateToken(user, request, response);
// The property is updated, but it won't be taking into account
@@ -150,44 +159,27 @@ public class JwtHttpHandlerIT {
verifyToken(jwtArgumentCaptor.getAllValues().get(1), user, firstSessionTimeoutInMinutes * 60, NOW);
}
- @Test
- public void session_timeout_property_cannot_be_zero() {
- settings.setProperty("sonar.web.sessionTimeoutInMinutes", 0);
-
- assertThatThrownBy(() -> new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Property sonar.web.sessionTimeoutInMinutes must be higher than 5 minutes and must not be greater than 3 months. Got 0 minutes");
- }
-
- @Test
- public void session_timeout_property_cannot_be_negative() {
- settings.setProperty("sonar.web.sessionTimeoutInMinutes", -10);
-
- assertThatThrownBy(() -> new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Property sonar.web.sessionTimeoutInMinutes must be higher than 5 minutes and must not be greater than 3 months. Got -10 minutes");
- }
-
- @Test
- public void session_timeout_property_cannot_be_set_to_five_minutes() {
- settings.setProperty("sonar.web.sessionTimeoutInMinutes", 5);
+ @ParameterizedTest
+ @ValueSource(ints = {-10, 0, 4 * 30 * 24 * 60})
+ void inactive_session_timeout_must_be_valid(int minutes) {
+ settings.setProperty("sonar.web.sessionTimeoutInMinutes", minutes);
+ Configuration config = settings.asConfig();
- assertThatThrownBy(() -> new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Property sonar.web.sessionTimeoutInMinutes must be higher than 5 minutes and must not be greater than 3 months. Got 5 minutes");
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> new JwtHttpHandler(system2, dbClient, config, jwtSerializer, jwtCsrfVerifier, activeTimeoutProvider))
+ .withMessage("Property sonar.web.sessionTimeoutInMinutes must be at least 6 minutes and must not be greater than 90 days (129 600 minutes). Got " + minutes + " minutes");
}
@Test
- public void session_timeout_property_cannot_be_greater_than_three_months() {
- settings.setProperty("sonar.web.sessionTimeoutInMinutes", 4 * 30 * 24 * 60);
+ void inactive_session_timeout_property_can_be_6_minute() {
+ settings.setProperty("sonar.web.sessionTimeoutInMinutes", 6);
+ Configuration config = settings.asConfig();
- assertThatThrownBy(() -> new JwtHttpHandler(system2, dbClient, settings.asConfig(), jwtSerializer, jwtCsrfVerifier))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Property sonar.web.sessionTimeoutInMinutes must be higher than 5 minutes and must not be greater than 3 months. Got 172800 minutes");
+ assertThatNoException().isThrownBy(() -> new JwtHttpHandler(system2, dbClient, config, jwtSerializer, jwtCsrfVerifier, activeTimeoutProvider));
}
@Test
- public void validate_token() {
+ void validate_token() {
UserDto user = db.users().insertUser();
addJwtCookie();
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
@@ -200,7 +192,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_refresh_session_when_refresh_time_is_reached() {
+ void validate_token_refresh_session_when_refresh_time_is_reached() {
UserDto user = db.users().insertUser();
addJwtCookie();
// Token was created 10 days ago and refreshed 6 minutes ago
@@ -219,7 +211,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_not_refresh_session_when_refresh_time_is_not_reached() {
+ void validate_token_does_not_refresh_session_when_refresh_time_is_not_reached() {
UserDto user = db.users().insertUser();
addJwtCookie();
// Token was created 10 days ago and refreshed 4 minutes ago
@@ -235,7 +227,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_not_refresh_session_when_disconnected_timeout_is_reached() {
+ void validate_token_does_not_refresh_session_when_disconnected_timeout_is_reached() {
UserDto user = db.users().insertUser();
addJwtCookie();
// Token was created 4 months ago, refreshed 4 minutes ago, and it expired in 5 minutes
@@ -249,7 +241,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_not_refresh_session_when_user_is_disabled() {
+ void validate_token_does_not_refresh_session_when_user_is_disabled() {
addJwtCookie();
UserDto user = addUser(false);
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
@@ -260,7 +252,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_not_refresh_session_when_token_is_no_more_valid() {
+ void validate_token_does_not_refresh_session_when_token_is_no_more_valid() {
addJwtCookie();
when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.empty());
@@ -269,7 +261,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_nothing_when_no_jwt_cookie() {
+ void validate_token_does_nothing_when_no_jwt_cookie() {
underTest.validateToken(request, response);
verifyNoInteractions(httpSession, jwtSerializer);
@@ -277,7 +269,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_nothing_when_empty_value_in_jwt_cookie() {
+ void validate_token_does_nothing_when_empty_value_in_jwt_cookie() {
when(request.getCookies()).thenReturn(new Cookie[] {new JakartaHttpRequest.JakartaCookie(new jakarta.servlet.http.Cookie("JWT-SESSION", ""))});
underTest.validateToken(request, response);
@@ -287,7 +279,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_verify_csrf_state() {
+ void validate_token_verify_csrf_state() {
UserDto user = db.users().insertUser();
addJwtCookie();
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
@@ -302,7 +294,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_nothing_when_no_session_token_in_db() {
+ void validate_token_does_nothing_when_no_session_token_in_db() {
UserDto user = db.users().insertUser();
addJwtCookie();
// No SessionToken in DB
@@ -317,7 +309,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_does_nothing_when_expiration_date_from_session_token_is_expired() {
+ void validate_token_does_nothing_when_expiration_date_from_session_token_is_expired() {
UserDto user = db.users().insertUser();
addJwtCookie();
// In SessionToken, the expiration date is expired...
@@ -334,7 +326,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void validate_token_refresh_state_when_refreshing_token() {
+ void validate_token_refresh_state_when_refreshing_token() {
UserDto user = db.users().insertUser();
addJwtCookie();
// Token was created 10 days ago and refreshed 6 minutes ago
@@ -351,7 +343,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void remove_token() {
+ void remove_token() {
addJwtCookie();
UserDto user = db.users().insertUser();
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
@@ -368,7 +360,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void does_not_remove_token_from_db_when_no_jwt_token_in_cookie() {
+ void does_not_remove_token_from_db_when_no_jwt_token_in_cookie() {
addJwtCookie();
UserDto user = db.users().insertUser();
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
@@ -382,7 +374,7 @@ public class JwtHttpHandlerIT {
}
@Test
- public void does_not_remove_token_from_db_when_no_cookie() {
+ void does_not_remove_token_from_db_when_no_cookie() {
UserDto user = db.users().insertUser();
SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ActiveTimeoutProvider.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ActiveTimeoutProvider.java
new file mode 100644
index 00000000000..cf408de49cd
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ActiveTimeoutProvider.java
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.authentication;
+
+import java.time.Duration;
+
+public interface ActiveTimeoutProvider {
+ Duration getActiveSessionTimeout();
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HardcodedActiveTimeoutProvider.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HardcodedActiveTimeoutProvider.java
new file mode 100644
index 00000000000..e8e40cd6f60
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HardcodedActiveTimeoutProvider.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.authentication;
+
+import java.time.Duration;
+
+public class HardcodedActiveTimeoutProvider implements ActiveTimeoutProvider {
+ public static final Duration DEFAULT_ACTIVE_TIMEOUT_DURATION = Duration.ofDays(90);
+
+ @Override
+ public Duration getActiveSessionTimeout() {
+ return DEFAULT_ACTIVE_TIMEOUT_DURATION;
+ }
+}
+
+
+
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
index ec640a9b11f..216a0c92fa2 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
@@ -21,6 +21,7 @@ package org.sonar.server.authentication;
import com.google.common.collect.ImmutableMap;
import io.jsonwebtoken.Claims;
+import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
@@ -41,7 +42,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.commons.lang3.time.DateUtils.addSeconds;
-import static org.sonar.process.ProcessProperties.Property.WEB_SESSION_TIMEOUT_IN_MIN;
+import static org.sonar.process.ProcessProperties.Property.WEB_INACTIVE_SESSION_TIMEOUT_IN_MIN;
import static org.sonar.server.authentication.Cookies.SAMESITE_LAX;
import static org.sonar.server.authentication.Cookies.SET_COOKIE;
import static org.sonar.server.authentication.Cookies.findCookie;
@@ -50,39 +51,40 @@ import static org.sonar.server.authentication.JwtSerializer.LAST_REFRESH_TIME_PA
@ServerSide
public class JwtHttpHandler {
- private static final int SESSION_TIMEOUT_DEFAULT_VALUE_IN_MINUTES = 3 * 24 * 60;
- private static final int MAX_SESSION_TIMEOUT_IN_MINUTES = 3 * 30 * 24 * 60;
+ private static final Duration DEFAULT_INACTIVE_TIMEOUT_DURATION = Duration.ofDays(3);
+ private static final Duration MINIMUM_INACTIVE_TIMEOUT_DURATION = Duration.ofMinutes(5);
+ private static final Duration MAXIMUM_INACTIVE_TIMEOUT_DURATION = Duration.ofDays(90);
private static final String JWT_COOKIE = "JWT-SESSION";
-
private static final String CSRF_JWT_PARAM = "xsrfToken";
- // Time after which a user will be disconnected
- private static final int SESSION_DISCONNECT_IN_SECONDS = 3 * 30 * 24 * 60 * 60;
-
// This refresh time is used to refresh the session
- // The value must be lower than sessionTimeoutInSeconds
- private static final int SESSION_REFRESH_IN_SECONDS = 5 * 60;
+ // The value must be lower than inactiveSessionTimeoutInSeconds
+ private static final Duration SESSION_REFRESH = Duration.ofMinutes(5);
private final System2 system2;
private final DbClient dbClient;
private final JwtSerializer jwtSerializer;
// This timeout is used to disconnect the user we he has not browse any page for a while
- private final int sessionTimeoutInSeconds;
+ private final Duration inactiveSessionTimeout;
+ // This timeout is used to disconnect the user regardless of his activity after logging in
+ private final Duration activeSessionTimeout;
private final JwtCsrfVerifier jwtCsrfVerifier;
- public JwtHttpHandler(System2 system2, DbClient dbClient, Configuration config, JwtSerializer jwtSerializer, JwtCsrfVerifier jwtCsrfVerifier) {
+ public JwtHttpHandler(System2 system2, DbClient dbClient, Configuration config, JwtSerializer jwtSerializer,
+ JwtCsrfVerifier jwtCsrfVerifier, ActiveTimeoutProvider activeTimeoutProvider) {
this.jwtSerializer = jwtSerializer;
this.dbClient = dbClient;
this.system2 = system2;
- this.sessionTimeoutInSeconds = getSessionTimeoutInSeconds(config);
+ this.inactiveSessionTimeout = getInactiveSessionTimeout(config);
+ this.activeSessionTimeout = activeTimeoutProvider.getActiveSessionTimeout();
this.jwtCsrfVerifier = jwtCsrfVerifier;
}
public void generateToken(UserDto user, Map<String, Object> properties, HttpRequest request, HttpResponse response) {
- String csrfState = jwtCsrfVerifier.generateState(request, response, sessionTimeoutInSeconds);
- long expirationTime = system2.now() + sessionTimeoutInSeconds * 1000L;
+ String csrfState = jwtCsrfVerifier.generateState(request, response, (int) inactiveSessionTimeout.toSeconds());
+ long expirationTime = system2.now() + (int) inactiveSessionTimeout.toSeconds() * 1000L;
SessionTokenDto sessionToken = createSessionToken(user, expirationTime);
String token = jwtSerializer.encode(new JwtSerializer.JwtSession(
@@ -94,7 +96,7 @@ public class JwtHttpHandler {
.put(LAST_REFRESH_TIME_PARAM, system2.now())
.put(CSRF_JWT_PARAM, csrfState)
.build()));
- response.addHeader(SET_COOKIE, createJwtSession(request, JWT_COOKIE, token, sessionTimeoutInSeconds));
+ response.addHeader(SET_COOKIE, createJwtSession(request, JWT_COOKIE, token, (int) inactiveSessionTimeout.toSeconds()));
}
private SessionTokenDto createSessionToken(UserDto user, long expirationTime) {
@@ -156,12 +158,12 @@ public class JwtHttpHandler {
if (now.getTime() > sessionToken.get().getExpirationDate()) {
return Optional.empty();
}
- if (now.after(addSeconds(token.getIssuedAt(), SESSION_DISCONNECT_IN_SECONDS))) {
+ if (now.after(addSeconds(token.getIssuedAt(), (int) activeSessionTimeout.toSeconds()))) {
return Optional.empty();
}
jwtCsrfVerifier.verifyState(request, (String) token.get(CSRF_JWT_PARAM), token.getSubject());
- if (now.after(addSeconds(getLastRefreshDate(token), SESSION_REFRESH_IN_SECONDS))) {
+ if (now.after(addSeconds(getLastRefreshDate(token), (int) SESSION_REFRESH.toSeconds()))) {
refreshToken(dbSession, sessionToken.get(), token, request, response);
}
@@ -176,10 +178,10 @@ public class JwtHttpHandler {
}
private void refreshToken(DbSession dbSession, SessionTokenDto tokenFromDb, Claims tokenFromCookie, HttpRequest request, HttpResponse response) {
- long expirationTime = system2.now() + sessionTimeoutInSeconds * 1000L;
+ long expirationTime = system2.now() + (int) inactiveSessionTimeout.toSeconds() * 1000L;
String refreshToken = jwtSerializer.refresh(tokenFromCookie, expirationTime);
- response.addHeader(SET_COOKIE, createJwtSession(request, JWT_COOKIE, refreshToken, sessionTimeoutInSeconds));
- jwtCsrfVerifier.refreshState(request, response, (String) tokenFromCookie.get(CSRF_JWT_PARAM), sessionTimeoutInSeconds);
+ response.addHeader(SET_COOKIE, createJwtSession(request, JWT_COOKIE, refreshToken, (int) inactiveSessionTimeout.toSeconds()));
+ jwtCsrfVerifier.refreshState(request, response, (String) tokenFromCookie.get(CSRF_JWT_PARAM), (int) inactiveSessionTimeout.toSeconds());
dbClient.sessionTokensDao().update(dbSession, tokenFromDb.setExpirationDate(expirationTime));
dbSession.commit();
@@ -219,12 +221,12 @@ public class JwtHttpHandler {
return Optional.ofNullable(user != null && user.isActive() ? user : null);
}
- private static int getSessionTimeoutInSeconds(Configuration config) {
- int minutes = config.getInt(WEB_SESSION_TIMEOUT_IN_MIN.getKey()).orElse(SESSION_TIMEOUT_DEFAULT_VALUE_IN_MINUTES);
- checkArgument(minutes > SESSION_REFRESH_IN_SECONDS / 60 && minutes <= MAX_SESSION_TIMEOUT_IN_MINUTES,
- "Property %s must be higher than 5 minutes and must not be greater than 3 months. Got %s minutes", WEB_SESSION_TIMEOUT_IN_MIN.getKey(),
- minutes);
- return minutes * 60;
+ private static Duration getInactiveSessionTimeout(Configuration config) {
+ String key = WEB_INACTIVE_SESSION_TIMEOUT_IN_MIN.getKey();
+ int minutes = config.getInt(key).orElse((int) DEFAULT_INACTIVE_TIMEOUT_DURATION.toMinutes());
+ checkArgument(minutes > MINIMUM_INACTIVE_TIMEOUT_DURATION.toMinutes() && minutes <= MAXIMUM_INACTIVE_TIMEOUT_DURATION.toMinutes(),
+ "Property %s must be at least 6 minutes and must not be greater than 90 days (129 600 minutes). Got %s minutes", key, minutes);
+ return Duration.ofMinutes(minutes);
}
public static class Token {
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java
index 1d3e9eaed18..e3fc380c07f 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposer.java
@@ -79,7 +79,7 @@ public class TokenExpirationEmailComposer extends EmailSender<TokenExpirationEma
format("<br/>If this token is still needed, please consider <a href=\"%s/account/security/\">generating</a> an equivalent.<br/><br/>", server.getPublicRootUrl()))
.append("Don't forget to update the token in the locations where it is in use. "
+ "This may include the CI pipeline that analyzes your projects, "
- + "the IDE settings that connect SonarLint to SonarQube, "
+ + "the IDE settings that connect SonarQube IDE to SonarQube Server, "
+ "and any places where you make calls to web services.");
return builder.toString();
}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/HardcodedActiveTimeoutProviderTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/HardcodedActiveTimeoutProviderTest.java
new file mode 100644
index 00000000000..6e2bd66b73d
--- /dev/null
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/HardcodedActiveTimeoutProviderTest.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.authentication;
+
+import java.time.Duration;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class HardcodedActiveTimeoutProviderTest {
+ private ActiveTimeoutProvider underTest;
+
+ @BeforeEach
+ void setUp() {
+ underTest = new HardcodedActiveTimeoutProvider();
+ }
+
+ @Test
+ void getActiveTimeoutInMinutes_whenSessionTimeoutIsNotConfigured_returns90Days() {
+ assertThat(underTest.getActiveSessionTimeout()).isEqualTo(Duration.ofDays(90));
+ }
+}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java
index ba45ed43dc4..d0ba6ca7224 100644
--- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/notification/TokenExpirationEmailComposerTest.java
@@ -70,7 +70,7 @@ class TokenExpirationEmailComposerTest {
+ "Last used on: January 01, 2022<br/>"
+ "Expires on: %s<br/><br/>"
+ "If this token is still needed, please consider <a href=\"http://localhost/account/security/\">generating</a> an equivalent.<br/><br/>"
- + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarLint to SonarQube, and any places where you make calls to web services.",
+ + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarQube IDE to SonarQube Server, and any places where you make calls to web services.",
parseDate(expiredDate), parseDate(expiredDate)));
}
@@ -91,7 +91,7 @@ class TokenExpirationEmailComposerTest {
+ "Last used on: January 01, 2022<br/>"
+ "Expired on: %s<br/><br/>"
+ "If this token is still needed, please consider <a href=\"http://localhost/account/security/\">generating</a> an equivalent.<br/><br/>"
- + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarLint to SonarQube, and any places where you make calls to web services.",
+ + "Don't forget to update the token in the locations where it is in use. This may include the CI pipeline that analyzes your projects, the IDE settings that connect SonarQube IDE to SonarQube Server, and any places where you make calls to web services.",
parseDate(expiredDate)));
}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupSearchRequest.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupSearchRequest.java
index 1ef6af378a1..816b971f540 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupSearchRequest.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupSearchRequest.java
@@ -24,6 +24,8 @@ import javax.annotation.Nullable;
public record GroupSearchRequest(
@Nullable String query,
@Nullable Boolean managed,
+ @Nullable String userUuid,
+ @Nullable String excludedUserUuid,
int page,
int pageSize
) {
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java
index fac6c4bb711..2801c905d82 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/group/service/GroupService.java
@@ -93,6 +93,8 @@ public class GroupService {
return GroupQuery.builder()
.searchText(groupSearchRequest.query())
.isManagedClause(getManagedInstanceSql(groupSearchRequest.managed()))
+ .userId(groupSearchRequest.userUuid())
+ .excludedUserId(groupSearchRequest.excludedUserUuid())
.build();
}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/GitUrlParser.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/GitUrlParser.java
new file mode 100644
index 00000000000..59bc0eb2e76
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/GitUrlParser.java
@@ -0,0 +1,222 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.common.projectbindings.service;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+
+import static org.sonar.server.exceptions.BadRequestException.throwBadRequestException;
+
+/**
+ * Generic parser for Git repository URLs.
+ *
+ * <p>This parser extracts organization, project, and repository information from repository URLs
+ * without trying to identify the specific DevOps platform. It focuses on parsing the path
+ * components after the domain to work with any Git hosting platform including enterprise instances.</p>
+ *
+ * <p>Supported URL patterns:</p>
+ * <ul>
+ * <li><strong>Standard format:</strong> http(s)://domain.com/org/repo</li>
+ * <li><strong>With project:</strong> http(s)://domain.com/org/project/_git/repo (Azure DevOps style)</li>
+ * <li><strong>Nested groups:</strong> http(s)://domain.com/group/subgroup/repo (GitLab style)</li>
+ * <li><strong>SSH format:</strong> git@domain.com:org/repo</li>
+ * <li><strong>SSH with project:</strong> git@domain.com:v3/org/project/repo (Azure DevOps style)</li>
+ * </ul>
+ */
+public final class GitUrlParser {
+
+ // SSH-only patterns (HTTP/HTTPS handled via URI parsing)
+ private static final Pattern SSH_STANDARD_PATTERN = Pattern.compile("^git@([^:]+):([^/]+)/([^/]+?)(?:\\.git)?/*$");
+ private static final Pattern SSH_AZURE_PATTERN = Pattern.compile("^git@([^:]+):v3/([^/]+)/([^/]+)/([^/]+?)(?:\\.git)?/*$");
+ private static final Pattern SSH_NESTED_PATTERN = Pattern.compile("^git@([^:]+):([^/]+/.+)/([^/]+?)(?:\\.git)?/*$");
+
+ // Path-only patterns for HTTP/HTTPS URLs
+ private static final Pattern PATH_STANDARD_PATTERN = Pattern.compile("^/([^/]+)/([^/]+?)(?:\\.git)?/*$");
+ private static final Pattern PATH_AZURE_PATTERN = Pattern.compile("^/([^/]+)/([^/]+)/_git/([^/]+?)(?:\\.git)?/*$");
+ private static final Pattern PATH_NESTED_PATTERN = Pattern.compile("^/([^/]+/.+)/([^/]+?)(?:\\.git)?/*$");
+
+ private static final String GIT_SUFFIX = ".git";
+ private static final String HTTP_PREFIX = "http://";
+ private static final String HTTPS_PREFIX = "https://";
+ private static final String SSH_PREFIX = "git@";
+
+ private GitUrlParser() {
+ // Utility class, no instantiation
+ }
+
+ /**
+ * Parses a Git repository URL to extract path components.
+ *
+ * @param url The Git repository URL to parse
+ * @return RepositoryInfo if the URL matches a supported format
+ * @throws org.sonar.server.exceptions.BadRequestException if the URL is null or empty
+ */
+ public static Optional<RepositoryInfo> parseRepositoryUrl(@Nullable String url) {
+ validateInput(url);
+ String trimmedUrl = url.trim();
+
+ // Protocol detection upfront
+ if (trimmedUrl.startsWith(SSH_PREFIX)) {
+ return parseSshUrl(trimmedUrl);
+ }
+
+ if (trimmedUrl.startsWith(HTTPS_PREFIX) || trimmedUrl.startsWith(HTTP_PREFIX)) {
+ return parseHttpUrl(trimmedUrl);
+ }
+
+ // Fallback for other formats
+ return Optional.empty();
+ }
+
+ /**
+ * Validates the input URL.
+ */
+ private static void validateInput(@Nullable String url) {
+ if (StringUtils.isBlank(url)) {
+ throwBadRequestException("URL cannot be empty");
+ }
+ }
+
+ /**
+ * Parses SSH URLs using regex patterns.
+ */
+ private static Optional<RepositoryInfo> parseSshUrl(String url) {
+ return tryPattern(SSH_AZURE_PATTERN, url, GitUrlParser::parseAzureMatch)
+ .or(() -> tryPattern(SSH_NESTED_PATTERN, url, GitUrlParser::parseNestedMatch))
+ .or(() -> tryPattern(SSH_STANDARD_PATTERN, url, GitUrlParser::parseStandardMatch));
+ }
+
+ /**
+ * Parses HTTP/HTTPS URLs using URI parsing.
+ */
+ private static Optional<RepositoryInfo> parseHttpUrl(String url) {
+ try {
+ URI uri = new URI(url);
+ String path = uri.getPath();
+
+ if (StringUtils.isBlank(path) || path.equals("/")) {
+ return Optional.empty();
+ }
+
+ return parsePathComponents(path);
+ } catch (URISyntaxException e) {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Parses path components extracted from URI (much simpler than full URL regex).
+ */
+ private static Optional<RepositoryInfo> parsePathComponents(String path) {
+ return tryPattern(PATH_AZURE_PATTERN, path, GitUrlParser::parseAzureMatch)
+ .or(() -> tryPattern(PATH_NESTED_PATTERN, path, GitUrlParser::parseNestedMatch))
+ .or(() -> tryPattern(PATH_STANDARD_PATTERN, path, GitUrlParser::parseStandardMatch));
+ }
+
+ /**
+ * Tries to match a pattern against input and applies a function to the matcher if successful.
+ */
+ private static Optional<RepositoryInfo> tryPattern(Pattern pattern, String input, Function<Matcher, RepositoryInfo> matchFunction) {
+ Matcher matcher = pattern.matcher(input);
+ return matcher.matches() ? Optional.of(matchFunction.apply(matcher)) : Optional.empty();
+ }
+
+ /**
+ * Parses Azure DevOps format matches (org/project/_git/repo or org/project/repo).
+ */
+ private static RepositoryInfo parseAzureMatch(Matcher matcher) {
+ if (matcher.groupCount() == 4) {
+ // SSH: git@host:v3/org/project/repo
+ return new RepositoryInfo(matcher.group(2), removeGitSuffix(matcher.group(4)), matcher.group(3));
+ } else {
+ // HTTP: /org/project/_git/repo
+ return new RepositoryInfo(matcher.group(1), removeGitSuffix(matcher.group(3)), matcher.group(2));
+ }
+ }
+
+ /**
+ * Parses nested format matches (org/suborg/repo).
+ */
+ private static RepositoryInfo parseNestedMatch(Matcher matcher) {
+ if (matcher.groupCount() == 3) {
+ // SSH: git@host:org/suborg/repo
+ return new RepositoryInfo(matcher.group(2), removeGitSuffix(matcher.group(3)));
+ } else {
+ // HTTP: /org/suborg/repo
+ return new RepositoryInfo(matcher.group(1), removeGitSuffix(matcher.group(2)));
+ }
+ }
+
+ /**
+ * Parses standard format matches (org/repo).
+ */
+ private static RepositoryInfo parseStandardMatch(Matcher matcher) {
+ if (matcher.groupCount() == 3) {
+ // SSH: git@host:org/repo
+ return new RepositoryInfo(matcher.group(2), removeGitSuffix(matcher.group(3)));
+ } else {
+ // HTTP: /org/repo
+ return new RepositoryInfo(matcher.group(1), removeGitSuffix(matcher.group(2)));
+ }
+ }
+
+ /**
+ * Removes .git suffix and trailing slashes from URLs.
+ */
+ private static String removeGitSuffix(String url) {
+ if (url.endsWith(GIT_SUFFIX)) {
+ url = url.substring(0, url.length() - GIT_SUFFIX.length());
+ }
+ // Remove trailing slashes
+ while (url.endsWith("/")) {
+ url = url.substring(0, url.length() - 1);
+ }
+ return url;
+ }
+
+ /**
+ * Information extracted from a parsed Git repository URL.
+ *
+ * @param organization The organization, workspace, group, or namespace name
+ * @param repository The repository name
+ * @param project The project name (used by Azure DevOps, null for other platforms)
+ */
+ public record RepositoryInfo(String organization, String repository, @Nullable String project) {
+
+ RepositoryInfo(String organization, String repository) {
+ this(organization, repository, null);
+ }
+
+ public String slug() {
+ return organization + "/" + repository;
+ }
+
+ public String projectName() {
+ return StringUtils.defaultString(project);
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingSearchStrategy.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingSearchStrategy.java
new file mode 100644
index 00000000000..b33a3cb4ab3
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingSearchStrategy.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.common.projectbindings.service;
+
+import java.util.List;
+import java.util.function.Function;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.db.alm.setting.ProjectAlmSettingQuery;
+
+/**
+ * Enum-based strategy for searching project bindings based on Git repository information.
+ * Each ALM platform (GitHub, Azure DevOps, Bitbucket) has different storage patterns for repository information in the database.
+ */
+public enum ProjectBindingSearchStrategy {
+
+ /**
+ * GitHub search strategy.
+ * GitHub stores repository information in "org/repo" format in the alm_repo column.
+ */
+ GITHUB(info -> ProjectAlmSettingQuery.forAlmRepo(info.slug())),
+
+ /**
+ * Azure DevOps search strategy.
+ * Azure DevOps stores project name in alm_slug and repository name in alm_repo.
+ */
+ AZURE_DEVOPS(info -> ProjectAlmSettingQuery.forAlmRepoAndSlug(info.repository(), info.projectName())),
+
+ /**
+ * Bitbucket search strategy.
+ * Bitbucket stores repository name in the alm_repo column.
+ */
+ BITBUCKET(info -> ProjectAlmSettingQuery.forAlmRepo(info.repository()));
+
+ private final Function<GitUrlParser.RepositoryInfo, ProjectAlmSettingQuery> queryBuilder;
+
+ ProjectBindingSearchStrategy(Function<GitUrlParser.RepositoryInfo, ProjectAlmSettingQuery> queryBuilder) {
+ this.queryBuilder = queryBuilder;
+ }
+
+ /**
+ * Searches for project ALM settings based on the provided repository information.
+ *
+ * @param dbClient the database client
+ * @param session the database session
+ * @param repositoryInfo parsed Git repository information
+ * @return list of matching project ALM settings
+ */
+ public List<ProjectAlmSettingDto> search(DbClient dbClient, DbSession session, GitUrlParser.RepositoryInfo repositoryInfo) {
+ ProjectAlmSettingQuery query = queryBuilder.apply(repositoryInfo);
+ return dbClient.projectAlmSettingDao().selectProjectAlmSettings(session, query, 1, Integer.MAX_VALUE);
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java
index 0cb0101ade9..0180aeb2186 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java
@@ -24,8 +24,7 @@ import javax.annotation.Nullable;
public record ProjectBindingsSearchRequest(
@Nullable String repository,
@Nullable String dopSettingId,
+ @Nullable String repositoryUrl,
Integer page,
- Integer pageSize
-) {
-
+ Integer pageSize) {
}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java
index 5bbd97fdbf9..2ab7d39431e 100644
--- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java
@@ -19,14 +19,18 @@
*/
package org.sonar.server.common.projectbindings.service;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingQuery;
import org.sonar.db.project.ProjectDto;
@@ -49,6 +53,12 @@ public class ProjectBindingsService {
}
public SearchResults<ProjectBindingInformation> findProjectBindingsByRequest(ProjectBindingsSearchRequest request) {
+ // Check if repository URL is provided for Git URL search
+ if (StringUtils.isNotBlank(request.repositoryUrl())) {
+ return findProjectBindingsByGitUrl(request.repositoryUrl());
+ }
+
+ // Use traditional repository name search
ProjectAlmSettingQuery query = buildProjectAlmSettingQuery(request);
try (DbSession session = dbClient.openSession(false)) {
int total = dbClient.projectAlmSettingDao().countProjectAlmSettings(session, query);
@@ -61,6 +71,8 @@ public class ProjectBindingsService {
}
private static ProjectAlmSettingQuery buildProjectAlmSettingQuery(ProjectBindingsSearchRequest request) {
+ // Note: repositoryUrl is handled separately in findProjectBindingsByRequest
+ // This method only handles traditional repository name search
return new ProjectAlmSettingQuery(request.repository(), request.dopSettingId());
}
@@ -79,8 +91,13 @@ public class ProjectBindingsService {
private static Function<ProjectAlmSettingDto, ProjectBindingInformation> projectAlmSettingDtoToProjectBindingInformation(Map<String, ProjectDto> projectUuidToProject) {
return projectAlmSettingDto -> {
ProjectDto projectDto = projectUuidToProject.get(projectAlmSettingDto.getProjectUuid());
- return new ProjectBindingInformation(projectAlmSettingDto.getUuid(), projectAlmSettingDto.getAlmSettingUuid(), projectAlmSettingDto.getProjectUuid(), projectDto.getKey(),
- projectAlmSettingDto.getAlmRepo(), projectAlmSettingDto.getAlmSlug());
+ return new ProjectBindingInformation(
+ projectAlmSettingDto.getUuid(),
+ projectAlmSettingDto.getAlmSettingUuid(),
+ projectAlmSettingDto.getProjectUuid(),
+ projectDto.getKey(),
+ projectAlmSettingDto.getAlmRepo(),
+ projectAlmSettingDto.getAlmSlug());
};
}
@@ -90,4 +107,64 @@ public class ProjectBindingsService {
}
}
+ /**
+ * Searches for project bindings using a Git repository URL.
+ * This method performs fuzzy matching by searching for repository names in database fields.
+ *
+ * @param gitUrl The Git repository URL to search for
+ * @return SearchResults containing matching project bindings
+ */
+ public SearchResults<ProjectBindingInformation> findProjectBindingsByGitUrl(@Nullable String gitUrl) {
+ if (StringUtils.isBlank(gitUrl)) {
+ return new SearchResults<>(List.of(), 0);
+ }
+
+ Optional<GitUrlParser.RepositoryInfo> repositoryInfoOpt = GitUrlParser.parseRepositoryUrl(gitUrl);
+ if (repositoryInfoOpt.isEmpty()) {
+ return new SearchResults<>(List.of(), 0);
+ }
+
+ GitUrlParser.RepositoryInfo repositoryInfo = repositoryInfoOpt.get();
+
+ try (DbSession session = dbClient.openSession(false)) {
+ Set<ProjectAlmSettingDto> projectAlmSettings = searchProjectBindings(session, repositoryInfo);
+
+ Set<String> projectUuids = projectAlmSettings.stream()
+ .map(ProjectAlmSettingDto::getProjectUuid)
+ .collect(Collectors.toSet());
+
+ List<ProjectDto> projectDtos = dbClient.projectDao().selectByUuids(session, projectUuids);
+ Map<String, ProjectDto> projectUuidsToProject = projectDtos.stream()
+ .collect(Collectors.toMap(ProjectDto::getUuid, identity()));
+
+ List<ProjectBindingInformation> results = projectAlmSettings.stream()
+ .map(projectAlmSettingDtoToProjectBindingInformation(projectUuidsToProject))
+ .toList();
+
+ return new SearchResults<>(results, results.size());
+ }
+ }
+
+ private Set<ProjectAlmSettingDto> searchProjectBindings(DbSession session, GitUrlParser.RepositoryInfo repositoryInfo) {
+ Set<ProjectAlmSettingDto> allResults = new HashSet<>();
+ for (ALM alm : ALM.values()) {
+ ProjectBindingSearchStrategy strategy = getSearchStrategy(alm);
+ if (strategy != null) {
+ allResults.addAll(strategy.search(dbClient, session, repositoryInfo));
+ }
+ }
+
+ return allResults.stream().collect(Collectors.toSet());
+ }
+
+ private static ProjectBindingSearchStrategy getSearchStrategy(ALM alm) {
+ return switch (alm) {
+ case GITHUB -> ProjectBindingSearchStrategy.GITHUB;
+ case AZURE_DEVOPS -> ProjectBindingSearchStrategy.AZURE_DEVOPS;
+ case BITBUCKET, BITBUCKET_CLOUD -> ProjectBindingSearchStrategy.BITBUCKET;
+ // Skip GitLab for now
+ case GITLAB -> null;
+ };
+ }
+
}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java
index 02a37273efa..bafc6e7431d 100644
--- a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/group/service/GroupServiceTest.java
@@ -316,7 +316,7 @@ public class GroupServiceTest {
public void createGroup_whenNameAndDescriptionIsProvided_createsGroup() {
when(uuidFactory.create()).thenReturn("1234");
- GroupDto createdGroup = mockGroupDto();
+ GroupDto createdGroup = mockGroupDto();
when(dbClient.groupDao().insert(eq(dbSession), any())).thenReturn(createdGroup);
mockDefaultGroup();
groupService.createGroup(dbSession, "Name", "Description");
@@ -378,7 +378,7 @@ public class GroupServiceTest {
when(dbClient.groupDao().countByQuery(eq(dbSession), any())).thenReturn(300);
- SearchResults<GroupInformation> searchResults = groupService.search(dbSession, new GroupSearchRequest("query", null, 5, 24));
+ SearchResults<GroupInformation> searchResults = groupService.search(dbSession, new GroupSearchRequest("query", null, null, null, 5, 24));
assertThat(searchResults.total()).isEqualTo(300);
Map<String, GroupInformation> uuidToGroupInformation = searchResults.searchResults().stream()
@@ -390,11 +390,12 @@ public class GroupServiceTest {
assertThat(queryCaptor.getValue().getSearchText()).isEqualTo("%QUERY%");
assertThat(queryCaptor.getValue().getIsManagedSqlClause()).isNull();
}
+
@Test
public void search_whenPageSizeEquals0_returnsOnlyTotal() {
when(dbClient.groupDao().countByQuery(eq(dbSession), any())).thenReturn(10);
- SearchResults<GroupInformation> searchResults = groupService.search(dbSession, new GroupSearchRequest("query", null, 0, 24));
+ SearchResults<GroupInformation> searchResults = groupService.search(dbSession, new GroupSearchRequest("query", null, null, null, 0, 24));
assertThat(searchResults.total()).isEqualTo(10);
assertThat(searchResults.searchResults()).isEmpty();
@@ -406,7 +407,7 @@ public class GroupServiceTest {
mockManagedInstance();
when(dbClient.groupDao().selectByQuery(eq(dbSession), queryCaptor.capture(), anyInt(), anyInt())).thenReturn(List.of());
- groupService.search(dbSession, new GroupSearchRequest("query", true, 5, 24));
+ groupService.search(dbSession, new GroupSearchRequest("query", true, null, null, 5, 24));
assertThat(queryCaptor.getValue().getIsManagedSqlClause()).isEqualTo("managed_filter");
}
@@ -416,7 +417,7 @@ public class GroupServiceTest {
mockManagedInstance();
when(dbClient.groupDao().selectByQuery(eq(dbSession), queryCaptor.capture(), anyInt(), anyInt())).thenReturn(List.of());
- groupService.search(dbSession, new GroupSearchRequest("query", false, 5, 24));
+ groupService.search(dbSession, new GroupSearchRequest("query", false, null, null, 5, 24));
assertThat(queryCaptor.getValue().getIsManagedSqlClause()).isEqualTo("not_managed_filter");
}
@@ -424,7 +425,7 @@ public class GroupServiceTest {
@Test
public void search_whenInstanceNotManagedAndManagedIsTrue_throws() {
assertThatExceptionOfType(BadRequestException.class)
- .isThrownBy(() -> groupService.search(dbSession, new GroupSearchRequest("query", true, 5, 24)))
+ .isThrownBy(() -> groupService.search(dbSession, new GroupSearchRequest("query", true, null, null, 5, 24)))
.withMessage("The 'managed' parameter is only available for managed instances.");
}
@@ -434,6 +435,26 @@ public class GroupServiceTest {
when(managedInstanceService.getManagedGroupsSqlFilter(false)).thenReturn("not_managed_filter");
}
+ @Test
+ public void search_whenUserIdParameterProvided_addsItToQuery() {
+ when(dbClient.groupDao().selectByQuery(eq(dbSession), queryCaptor.capture(), anyInt(), anyInt())).thenReturn(List.of());
+
+ groupService.search(dbSession, new GroupSearchRequest("query", null, "includedUserUuid", null, 5, 24));
+
+ assertThat(queryCaptor.getValue().getUserId()).isEqualTo("includedUserUuid");
+ assertThat(queryCaptor.getValue().getExcludedUserId()).isNull();
+ }
+
+ @Test
+ public void search_whenExcludedUserIdParameterProvided_addsItToQuery() {
+ when(dbClient.groupDao().selectByQuery(eq(dbSession), queryCaptor.capture(), anyInt(), anyInt())).thenReturn(List.of());
+
+ groupService.search(dbSession, new GroupSearchRequest("query", null, null, "excludedUserId", 5, 24));
+
+ assertThat(queryCaptor.getValue().getUserId()).isNull();
+ assertThat(queryCaptor.getValue().getExcludedUserId()).isEqualTo("excludedUserId");
+ }
+
private static void assertGroupInformation(Map<String, GroupInformation> uuidToGroupInformation, GroupDto expectedGroupDto, boolean expectedManaged, boolean expectedDefault) {
assertThat(uuidToGroupInformation.get(expectedGroupDto.getUuid()).groupDto()).isEqualTo(expectedGroupDto);
assertThat(uuidToGroupInformation.get(expectedGroupDto.getUuid()).isManaged()).isEqualTo(expectedManaged);
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/GitUrlParserTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/GitUrlParserTest.java
new file mode 100644
index 00000000000..3caab3cb684
--- /dev/null
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/GitUrlParserTest.java
@@ -0,0 +1,280 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.common.projectbindings.service;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.sonar.server.common.projectbindings.service.GitUrlParser.RepositoryInfo;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+class GitUrlParserTest {
+
+ @ParameterizedTest
+ @MethodSource("successfulParsingCases")
+ void parseRepositoryUrl_shouldParseValidUrls(String url, String expectedOrg, String expectedRepo, @Nullable String expectedProject) {
+ Optional<RepositoryInfo> result = GitUrlParser.parseRepositoryUrl(url);
+
+ assertRepositoryInfo(result, expectedOrg, expectedRepo, expectedProject);
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ void parseRepositoryUrl_shouldThrowExceptionForNullOrEmptyUrls(@Nullable String url) {
+ assertThatThrownBy(() -> GitUrlParser.parseRepositoryUrl(url))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("URL cannot be empty");
+ }
+
+ @ParameterizedTest
+ @MethodSource("invalidUrls")
+ void parseRepositoryUrl_shouldReturnEmptyForInvalidUrls(@Nullable String url) {
+ assertThat(GitUrlParser.parseRepositoryUrl(url)).isEmpty();
+ }
+
+ private static void assertRepositoryInfo(Optional<RepositoryInfo> result, String expectedOrg, String expectedRepo, @Nullable String expectedProject) {
+ assertThat(result).isPresent();
+ RepositoryInfo info = result.get();
+
+ assertThat(info.organization()).isEqualTo(expectedOrg);
+ assertThat(info.repository()).isEqualTo(expectedRepo);
+ assertThat(info.project()).isEqualTo(expectedProject);
+ assertThat(info.slug()).isEqualTo(expectedOrg + "/" + expectedRepo);
+
+ if (expectedProject != null) {
+ assertThat(info.projectName()).isEqualTo(expectedProject);
+ } else {
+ assertThat(info.projectName()).isEmpty();
+ }
+ }
+
+ private static Stream<Arguments> successfulParsingCases() {
+ return Stream.of(
+ // ===== STANDARD FORMAT (org/repo) =====
+
+ // HTTP/HTTPS Standard - Basic cases
+ arguments("https://github.com/org/repo", "org", "repo", null),
+ arguments("http://github.com/org/repo", "org", "repo", null),
+ arguments("https://bitbucket.org/myorg/myrepo", "myorg", "myrepo", null),
+ arguments("https://gitlab.com/company/project", "company", "project", null),
+ arguments("http://git.company.com/team/service", "team", "service", null),
+
+ // HTTP/HTTPS Standard - With .git suffix
+ arguments("https://github.com/org/repo.git", "org", "repo", null),
+ arguments("http://gitlab.com/user/project.git", "user", "project", null),
+
+ // HTTP/HTTPS Standard - With trailing slashes
+ arguments("https://github.com/org/repo/", "org", "repo", null),
+ arguments("https://github.com/org/repo//", "org", "repo", null),
+ arguments("https://github.com/org/repo///", "org", "repo", null),
+ arguments("https://github.com/org/repo.git/", "org", "repo", null),
+ arguments("https://github.com/org/repo.git///", "org", "repo", null),
+
+ // HTTP/HTTPS Standard - With ports
+ arguments("https://git.company.com:8080/org/repo", "org", "repo", null),
+ arguments("http://gitlab.local:3000/org/repo.git", "org", "repo", null),
+ arguments("https://bitbucket.example.com:9443/team/service/", "team", "service", null),
+
+ // HTTP/HTTPS Standard - With authentication
+ arguments("https://user@github.com/org/repo", "org", "repo", null),
+ arguments("https://user:pass@gitlab.com/org/repo.git", "org", "repo", null),
+
+ // HTTP/HTTPS Standard - With query parameters
+ arguments("https://github.com/org/repo?branch=main", "org", "repo", null),
+ arguments("https://github.com/org/repo.git?ref=develop", "org", "repo", null),
+
+ // SSH Standard - Basic cases
+ arguments("git@github.com:org/repo", "org", "repo", null),
+ arguments("git@gitlab.com:user/project", "user", "project", null),
+ arguments("git@bitbucket.org:company/service", "company", "service", null),
+
+ // SSH Standard - With .git suffix
+ arguments("git@github.com:org/repo.git", "org", "repo", null),
+ arguments("git@gitlab.com:user/project.git", "user", "project", null),
+
+ // SSH Standard - With trailing slashes
+ arguments("git@github.com:org/repo/", "org", "repo", null),
+ arguments("git@github.com:org/repo//", "org", "repo", null),
+ arguments("git@github.com:org/repo.git/", "org", "repo", null),
+
+ // SSH Standard - Custom hosts
+ arguments("git@git.company.com:team/project", "team", "project", null),
+ arguments("git@gitlab.example.org:dept/app.git", "dept", "app", null),
+
+ // ===== AZURE DEVOPS FORMAT (org/project/_git/repo) =====
+
+ // HTTP/HTTPS Azure - Basic cases
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo", "myorg", "myrepo", "myproject"),
+ arguments("http://dev.azure.com/company/frontend/_git/webapp", "company", "webapp", "frontend"),
+ arguments("https://devops.company.com/team/backend/_git/api", "team", "api", "backend"),
+
+ // HTTP/HTTPS Azure - With .git suffix
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo.git", "myorg", "myrepo", "myproject"),
+ arguments("http://azure.example.com/org/proj/_git/service.git", "org", "service", "proj"),
+
+ // HTTP/HTTPS Azure - With trailing slashes
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo/", "myorg", "myrepo", "myproject"),
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo//", "myorg", "myrepo", "myproject"),
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo.git/", "myorg", "myrepo", "myproject"),
+ arguments("https://dev.azure.com/myorg/myproject/_git/myrepo.git///", "myorg", "myrepo", "myproject"),
+
+ // HTTP/HTTPS Azure - With ports
+ arguments("https://devops.company.com:8443/myorg/myproject/_git/myrepo", "myorg", "myrepo", "myproject"),
+ arguments("http://azure.local:8080/team/proj/_git/app.git", "team", "app", "proj"),
+
+ // SSH Azure - Basic cases
+ arguments("git@ssh.dev.azure.com:v3/myorg/myproject/myrepo", "myorg", "myrepo", "myproject"),
+ arguments("git@azure.company.com:v3/team/frontend/webapp", "team", "webapp", "frontend"),
+
+ // SSH Azure - With .git suffix
+ arguments("git@ssh.dev.azure.com:v3/myorg/myproject/myrepo.git", "myorg", "myrepo", "myproject"),
+ arguments("git@devops.example.com:v3/org/backend/api.git", "org", "api", "backend"),
+
+ // SSH Azure - With trailing slashes
+ arguments("git@ssh.dev.azure.com:v3/myorg/myproject/myrepo/", "myorg", "myrepo", "myproject"),
+ arguments("git@ssh.dev.azure.com:v3/myorg/myproject/myrepo//", "myorg", "myrepo", "myproject"),
+ arguments("git@ssh.dev.azure.com:v3/myorg/myproject/myrepo.git/", "myorg", "myrepo", "myproject"),
+
+ // ===== NESTED FORMAT (group/subgroup/repo) =====
+
+ // HTTP/HTTPS Nested - Basic cases
+ arguments("https://gitlab.com/group/subgroup/project", "group/subgroup", "project", null),
+ arguments("http://gitlab.com/company/team/service", "company/team", "service", null),
+ arguments("https://git.company.com/dept/division/app", "dept/division", "app", null),
+
+ // HTTP/HTTPS Nested - With .git suffix
+ arguments("https://gitlab.com/group/subgroup/project.git", "group/subgroup", "project", null),
+ arguments("http://gitlab.example.com/org/team/repo.git", "org/team", "repo", null),
+
+ // HTTP/HTTPS Nested - With trailing slashes
+ arguments("https://gitlab.com/group/subgroup/project/", "group/subgroup", "project", null),
+ arguments("https://gitlab.com/group/subgroup/project//", "group/subgroup", "project", null),
+ arguments("https://gitlab.com/group/subgroup/project.git/", "group/subgroup", "project", null),
+ arguments("https://gitlab.com/group/subgroup/project.git///", "group/subgroup", "project", null),
+
+ // HTTP/HTTPS Nested - With ports
+ arguments("https://gitlab.company.com:9443/group/subgroup/project", "group/subgroup", "project", null),
+ arguments("http://git.local:3000/team/division/app.git", "team/division", "app", null),
+
+ // HTTP/HTTPS Nested - Deep nesting
+ arguments("https://gitlab.com/org/team/dept/division/project", "org/team/dept/division", "project", null),
+ arguments("https://gitlab.com/a/b/c/d/e/f/repo.git", "a/b/c/d/e/f", "repo", null),
+
+ // SSH Nested - Basic cases
+ arguments("git@gitlab.com:group/subgroup/project", "group/subgroup", "project", null),
+ arguments("git@git.company.com:team/division/service", "team/division", "service", null),
+
+ // SSH Nested - With .git suffix
+ arguments("git@gitlab.com:group/subgroup/project.git", "group/subgroup", "project", null),
+ arguments("git@gitlab.example.com:org/team/app.git", "org/team", "app", null),
+
+ // SSH Nested - With trailing slashes
+ arguments("git@gitlab.com:group/subgroup/project/", "group/subgroup", "project", null),
+ arguments("git@gitlab.com:group/subgroup/project//", "group/subgroup", "project", null),
+ arguments("git@gitlab.com:group/subgroup/project.git/", "group/subgroup", "project", null),
+
+ // SSH Nested - Deep nesting
+ arguments("git@gitlab.com:org/team/dept/division/project.git", "org/team/dept/division", "project", null),
+
+ // ===== EDGE CASES =====
+
+ // URLs with special characters in names
+ arguments("https://github.com/my-org/my-repo", "my-org", "my-repo", null),
+ arguments("git@gitlab.com:user.name/project_name.git", "user.name", "project_name", null),
+ arguments("https://dev.azure.com/my-org/my-project/_git/my-repo", "my-org", "my-repo", "my-project"),
+
+ // Very long paths (potential exponential backtracking)
+ arguments(
+ "https://gitlab.com/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/NOTFOUND",
+ "a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a",
+ "NOTFOUND",
+ null),
+
+ // Mixed case scenarios
+ arguments("https://GitHub.COM/ORG/REPO.GIT", "ORG", "REPO.GIT", null), // .GIT (uppercase) is not removed
+
+ // Numeric organization/project names
+ arguments("https://github.com/123org/456repo", "123org", "456repo", null),
+ arguments("git@gitlab.com:999/project123.git", "999", "project123", null),
+
+ // URLs that might be mistaken as invalid but are parsed as nested format
+ arguments("https://dev.azure.com/org/_git/repo", "org/_git", "repo", null), // parsed as nested, not Azure
+ arguments("https://dev.azure.com/org/project/git/repo", "org/project/git", "repo", null), // parsed as nested
+ arguments("git@ssh.dev.azure.com:v3/org/repo", "v3/org", "repo", null), // parsed as nested, not Azure SSH
+ arguments("git@ssh.dev.azure.com:v2/org/project/repo", "v2/org/project", "repo", null), // parsed as nested
+ arguments("https://dev.azure.com/org/project/_git/", "org/project", "_git", null) // trailing slash parsed as nested
+ );
+ }
+
+ private static Stream<String> invalidUrls() {
+ return Stream.of(
+ // Unsupported protocols
+ "ftp://github.com/user/repo",
+ "svn://server.com/repo",
+ "file:///local/path/repo",
+
+ // Incomplete URLs
+ "https://github.com/", // missing org and repo
+ "https://github.com/org", // missing repo
+ "http://gitlab.com", // no path
+
+ // Invalid SSH formats
+ "git@", // incomplete SSH
+ "git@github.com", // missing colon and path
+ "git@github.com:", // missing path
+ "git@github.com:/", // invalid path
+
+ // Not URLs at all
+ "not-a-url-at-all",
+ "github.com/org/repo", // missing protocol
+ "www.github.com/org/repo", // missing protocol
+
+ // Invalid characters or malformed
+ "https://github.com/org repo/test", // space in path
+ "https://", // protocol only
+ "http://", // protocol only
+
+ // Just domain without path
+ "https://github.com",
+ "http://dev.azure.com",
+ "git@gitlab.com:",
+
+ // URLs with only root path
+ "https://github.com/",
+ "http://gitlab.com/",
+
+ // URLs that result in empty repo names
+ "https://github.com/org/", // missing repo (empty after slash removal)
+
+ // URLs that are too short (after reconsidering the actual parser behavior)
+ "https://a.com/b",
+ "git@a.com:b"
+ );
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java
index 77324fcd16f..9bb3f622793 100644
--- a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java
@@ -22,9 +22,13 @@ package org.sonar.server.common.projectbindings.service;
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import javax.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
@@ -102,7 +106,7 @@ public class ProjectBindingsServiceTest {
when(dbClient.projectDao().selectByUuids(dbSession, Set.of("projectUuid_1", "projectUuid_2")))
.thenReturn(List.of(mockProjectDto1, mockProjectDto2));
- ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(REPO_QUERY, ALM_SETTING_UUID_QUERY, 12, 42);
+ ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(REPO_QUERY, ALM_SETTING_UUID_QUERY, null, 12, 42);
List<ProjectBindingInformation> expectedResults = List.of(projectBindingInformation("1"), projectBindingInformation("2"));
@@ -119,7 +123,7 @@ public class ProjectBindingsServiceTest {
when(dbClient.projectAlmSettingDao().countProjectAlmSettings(eq(dbSession), any()))
.thenReturn(12);
- ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(null, null, 42, 0);
+ ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(null, null, null, 42, 0);
SearchResults<ProjectBindingInformation> actualResults = underTest.findProjectBindingsByRequest(request);
assertThat(actualResults.total()).isEqualTo(12);
@@ -128,6 +132,103 @@ public class ProjectBindingsServiceTest {
verify(dbClient.projectAlmSettingDao(), never()).selectProjectAlmSettings(eq(dbSession), any(), anyInt(), anyInt());
}
+ @Test
+ void findProjectBindingsByRequest_whenRepositoryUrlProvided_usesGitUrlSearch() {
+ when(dbClient.projectAlmSettingDao().selectProjectAlmSettings(eq(dbSession), any(), eq(1), eq(Integer.MAX_VALUE)))
+ .thenReturn(List.of());
+ when(dbClient.projectDao().selectByUuids(dbSession, Set.of()))
+ .thenReturn(List.of());
+
+ ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(null, null, "https://github.com/org/repo", 0, 50);
+
+ SearchResults<ProjectBindingInformation> actualResults = underTest.findProjectBindingsByRequest(request);
+
+ assertThat(actualResults.searchResults()).isEmpty();
+ assertThat(actualResults.total()).isZero();
+
+ verify(dbClient.projectAlmSettingDao(), never()).countProjectAlmSettings(eq(dbSession), any());
+ }
+
+ @Test
+ void findProjectFromBinding_whenProjectExists_returnsIt() {
+ ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class);
+ when(projectAlmSettingDto.getProjectUuid()).thenReturn("project-uuid-123");
+
+ ProjectDto projectDto = mock(ProjectDto.class);
+ when(dbClient.projectDao().selectByUuid(dbSession, "project-uuid-123")).thenReturn(Optional.of(projectDto));
+
+ Optional<ProjectDto> result = underTest.findProjectFromBinding(projectAlmSettingDto);
+
+ assertThat(result).isPresent().contains(projectDto);
+ }
+
+ @Test
+ void findProjectFromBinding_whenProjectDoesNotExist_returnsEmpty() {
+ ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class);
+ when(projectAlmSettingDto.getProjectUuid()).thenReturn("non-existent-uuid");
+
+ when(dbClient.projectDao().selectByUuid(dbSession, "non-existent-uuid")).thenReturn(Optional.empty());
+
+ Optional<ProjectDto> result = underTest.findProjectFromBinding(projectAlmSettingDto);
+
+ assertThat(result).isEmpty();
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = {" ", "not-a-valid-url"})
+ void findProjectBindingsByGitUrl_whenUrlIsInvalid_returnsEmptyResults(@Nullable String url) {
+ SearchResults<ProjectBindingInformation> result = underTest.findProjectBindingsByGitUrl(url);
+
+ assertThat(result.searchResults()).isEmpty();
+ assertThat(result.total()).isZero();
+ }
+
+ @Test
+ void findProjectBindingsByGitUrl_whenUrlIsValid_returnsResults() {
+ ProjectAlmSettingDto githubSetting = mockProjectAlmSettingDto("1");
+ ProjectAlmSettingDto azureSetting = mockProjectAlmSettingDto("2");
+
+ when(dbClient.projectAlmSettingDao().selectProjectAlmSettings(eq(dbSession), any(), eq(1), eq(Integer.MAX_VALUE)))
+ .thenReturn(List.of(githubSetting))
+ .thenReturn(List.of(azureSetting))
+ .thenReturn(List.of())
+ .thenReturn(List.of());
+
+ ProjectDto projectDto1 = mockProjectDto("1");
+ ProjectDto projectDto2 = mockProjectDto("2");
+ when(dbClient.projectDao().selectByUuids(dbSession, Set.of("projectUuid_1", "projectUuid_2")))
+ .thenReturn(List.of(projectDto1, projectDto2));
+
+ SearchResults<ProjectBindingInformation> result = underTest.findProjectBindingsByGitUrl("https://github.com/org/repo");
+
+ assertThat(result.searchResults()).hasSize(2);
+ assertThat(result.total()).isEqualTo(2);
+ assertThat(result.searchResults()).extracting(ProjectBindingInformation::id)
+ .containsExactlyInAnyOrder("uuid_1", "uuid_2");
+ }
+
+ @Test
+ void findProjectBindingsByGitUrl_whenDuplicateResults_removesThemAndReturnsDistinct() {
+ ProjectAlmSettingDto duplicatedSetting = mockProjectAlmSettingDto("1");
+
+ when(dbClient.projectAlmSettingDao().selectProjectAlmSettings(eq(dbSession), any(), eq(1), eq(Integer.MAX_VALUE)))
+ .thenReturn(List.of(duplicatedSetting))
+ .thenReturn(List.of(duplicatedSetting))
+ .thenReturn(List.of())
+ .thenReturn(List.of());
+
+ ProjectDto projectDto = mockProjectDto("1");
+ when(dbClient.projectDao().selectByUuids(dbSession, Set.of("projectUuid_1")))
+ .thenReturn(List.of(projectDto));
+
+ SearchResults<ProjectBindingInformation> result = underTest.findProjectBindingsByGitUrl("https://github.com/org/repo");
+
+ assertThat(result.searchResults()).hasSize(1);
+ assertThat(result.total()).isEqualTo(1);
+ assertThat(result.searchResults().get(0).id()).isEqualTo("uuid_1");
+ }
+
private static ProjectAlmSettingDto mockProjectAlmSettingDto(String i) {
ProjectAlmSettingDto dto = mock();
when(dto.getUuid()).thenReturn("uuid_" + i);
@@ -146,12 +247,7 @@ public class ProjectBindingsServiceTest {
}
private static ProjectBindingInformation projectBindingInformation(String i) {
- return new ProjectBindingInformation("uuid_" + i,
- "almSettingUuid_" + i,
- "projectUuid_" + i,
- "projectKey_" + i,
- "almRepo_" + i,
- "almSlug_" + i);
+ return new ProjectBindingInformation("uuid_" + i, "almSettingUuid_" + i, "projectUuid_" + i, "projectKey_" + i, "almRepo_" + i, "almSlug_" + i);
}
}
diff --git a/server/sonar-webserver-core/src/it/java/org/sonar/server/startup/RegisterMetricsIT.java b/server/sonar-webserver-core/src/it/java/org/sonar/server/startup/RegisterMetricsIT.java
index 1278205a383..814f5b2b20d 100644
--- a/server/sonar-webserver-core/src/it/java/org/sonar/server/startup/RegisterMetricsIT.java
+++ b/server/sonar-webserver-core/src/it/java/org/sonar/server/startup/RegisterMetricsIT.java
@@ -44,7 +44,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class RegisterMetricsIT {
- public static final int SOON_TO_BE_REMOVED_COMPLEXITY_METRICS_COUNT = 7;
@Rule
public DbTester dbTester = DbTester.create(System2.INSTANCE);
@@ -144,9 +143,7 @@ public class RegisterMetricsIT {
.isEqualTo(CoreMetrics.getMetrics().size()
// Metric CoreMetrics.WONT_FIX_ISSUES was renamed to CoreMetrics.ACCEPTED_ISSUES in 10.3.
// We don't want to insert it anymore
- - 1
- // SONAR-12647 We are exclusing complexity metrics, they will be removed from the plugin API soon
- - SOON_TO_BE_REMOVED_COMPLEXITY_METRICS_COUNT);
+ - 1);
}
@Test
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java
index a3533a80743..e124a467ae8 100644
--- a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/registration/RulesRegistrationContext.java
@@ -83,9 +83,11 @@ class RulesRegistrationContext {
String ruleUuid = entry.getKey();
RuleDto rule = dbRulesByRuleUuid.get(ruleUuid);
if (rule == null) {
- LOG.warn("Could not retrieve rule with uuid %s referenced by a deprecated rule key. " +
- "The following deprecated rule keys seem to be referencing a non-existing rule",
- ruleUuid, entry.getValue());
+ LOG.warn("Could not retrieve rule with uuid {} referenced by a deprecated rule key. " +
+ "The following deprecated rule keys seem to be referencing a non-existing rule: {}",
+ ruleUuid, entry.getValue().stream()
+ .map(SingleDeprecatedRuleKey::getOldRuleKeyAsRuleKey)
+ .collect(Collectors.toSet()));
} else {
entry.getValue().forEach(d -> rulesByKey.put(d.getOldRuleKeyAsRuleKey(), rule));
}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java
index 05fcb23d55c..08965b0c895 100644
--- a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java
@@ -20,11 +20,10 @@
package org.sonar.server.startup;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.FluentIterable;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import org.sonar.api.Startable;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Metric;
@@ -39,24 +38,12 @@ import org.sonar.db.metric.MetricDto;
import org.sonar.server.metric.MetricToDto;
import org.springframework.beans.factory.annotation.Autowired;
-import static com.google.common.collect.FluentIterable.concat;
import static com.google.common.collect.Lists.newArrayList;
import static org.sonar.db.metric.RemovedMetricConverter.REMOVED_METRIC;
public class RegisterMetrics implements Startable {
private static final Logger LOG = Loggers.get(RegisterMetrics.class);
- /**
- * Those metrics will be removed soon from the plugin API, so let's not register them in the database
- */
- private static final Set<String> SOON_TO_BE_REMOVED_FROM_CORE_API_METRIC = Set.of(
- CoreMetrics.COMPLEXITY_IN_CLASSES_KEY,
- CoreMetrics.COMPLEXITY_IN_FUNCTIONS_KEY,
- CoreMetrics.FILE_COMPLEXITY_DISTRIBUTION_KEY,
- CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION_KEY,
- CoreMetrics.FUNCTION_COMPLEXITY_KEY,
- CoreMetrics.CLASS_COMPLEXITY_KEY,
- CoreMetrics.FILE_COMPLEXITY_KEY);
private final DbClient dbClient;
private final UuidFactory uuidFactory;
@@ -79,8 +66,10 @@ public class RegisterMetrics implements Startable {
@Override
public void start() {
- FluentIterable<Metric> metricsToRegister = concat(getCoreMetrics(), getPluginMetrics())
- .filter(m -> !REMOVED_METRIC.equals(m.getKey()));
+ List<Metric> metricsToRegister = new ArrayList<>();
+ metricsToRegister.addAll(CoreMetrics.getMetrics());
+ metricsToRegister.addAll(getPluginMetrics());
+ metricsToRegister.removeIf(m -> REMOVED_METRIC.equals(m.getKey()));
register(metricsToRegister);
}
@@ -131,13 +120,6 @@ public class RegisterMetrics implements Startable {
}
}
- private static List<Metric> getCoreMetrics() {
- return CoreMetrics.getMetrics()
- .stream()
- .filter(m -> !SOON_TO_BE_REMOVED_FROM_CORE_API_METRIC.contains(m.getKey()))
- .toList();
- }
-
@VisibleForTesting
List<Metric> getPluginMetrics() {
List<Metric> metricsToRegister = newArrayList();
@@ -153,7 +135,7 @@ public class RegisterMetrics implements Startable {
private static void checkMetrics(Map<String, Metrics> metricsByRepository, Metrics metrics) {
for (Metric metric : metrics.getMetrics()) {
String metricKey = metric.getKey();
- if (getCoreMetrics().contains(metric)) {
+ if (CoreMetrics.getMetrics().contains(metric)) {
throw new IllegalStateException(String.format("Metric [%s] is already defined by SonarQube", metricKey));
}
Metrics anotherRepository = metricsByRepository.get(metricKey);
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/RulesRegistrationContextTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/RulesRegistrationContextTest.java
new file mode 100644
index 00000000000..4ea0c11e7c6
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/registration/RulesRegistrationContextTest.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.rule.registration;
+
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.slf4j.event.Level;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.rule.DeprecatedRuleKeyDto;
+import org.sonar.db.rule.RuleDao;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class RulesRegistrationContextTest {
+
+ @RegisterExtension
+ private final LogTesterJUnit5 logTester = new LogTesterJUnit5();
+
+ @Test
+ void whenDeprecatedRuleIsNotFound_thenWarningLogTraceIsProduced() {
+ DbClient dbClient = mock();
+ DbSession dbSession = mock();
+ RuleDao ruleDao = mock();
+ when(dbClient.ruleDao()).thenReturn(ruleDao);
+ when(ruleDao.selectAll(dbSession)).thenReturn(List.of());
+ when(ruleDao.selectAllDeprecatedRuleKeys(dbSession)).thenReturn(Set.of(
+ createDeprecatedRuleKeyDto("oldKey", "oldRepo", "newKey", "uuid")
+ ));
+ when(ruleDao.selectAllRuleParams(dbSession)).thenReturn(List.of());
+
+ RulesRegistrationContext.create(dbClient, dbSession);
+
+ assertThat(logTester.logs(Level.WARN)).
+ contains("Could not retrieve rule with uuid newKey referenced by a deprecated rule key. " +
+ "The following deprecated rule keys seem to be referencing a non-existing rule: [oldRepo:oldKey]");
+ }
+
+ private DeprecatedRuleKeyDto createDeprecatedRuleKeyDto(String oldKey, String oldRepo, String newKey, String uuid) {
+ DeprecatedRuleKeyDto dto = new DeprecatedRuleKeyDto();
+ dto.setOldRuleKey(oldKey);
+ dto.setOldRepositoryKey(oldRepo);
+ dto.setRuleUuid(newKey);
+ dto.setUuid(uuid);
+ return dto;
+ }
+}
diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java
index 72b3bd272f0..9235bc57b1a 100644
--- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java
+++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java
@@ -26,6 +26,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -54,6 +55,7 @@ import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.BucketOrder;
import org.elasticsearch.search.aggregations.HasAggregations;
+import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator;
import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilter;
@@ -77,12 +79,13 @@ import org.sonar.api.issue.IssueStatus;
import org.sonar.api.issue.impact.SoftwareQuality;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.CleanCodeAttributeCategory;
-import org.sonar.core.rule.RuleType;
import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.server.rule.RulesDefinition.OwaspMobileTop10Version;
import org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version;
import org.sonar.api.server.rule.RulesDefinition.PciDssVersion;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.System2;
+import org.sonar.core.rule.RuleType;
import org.sonar.server.es.EsClient;
import org.sonar.server.es.EsUtils;
import org.sonar.server.es.SearchOptions;
@@ -120,10 +123,10 @@ import static org.elasticsearch.index.query.QueryBuilders.termQuery;
import static org.elasticsearch.index.query.QueryBuilders.termsQuery;
import static org.elasticsearch.search.aggregations.AggregationBuilders.filters;
import static org.elasticsearch.search.aggregations.AggregationBuilders.reverseNested;
-import static org.sonar.core.rule.RuleType.SECURITY_HOTSPOT;
-import static org.sonar.core.rule.RuleType.VULNERABILITY;
import static org.sonar.core.config.MQRModeConstants.MULTI_QUALITY_MODE_DEFAULT_VALUE;
import static org.sonar.core.config.MQRModeConstants.MULTI_QUALITY_MODE_ENABLED;
+import static org.sonar.core.rule.RuleType.SECURITY_HOTSPOT;
+import static org.sonar.core.rule.RuleType.VULNERABILITY;
import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars;
import static org.sonar.server.es.IndexType.FIELD_INDEX_TYPE;
import static org.sonar.server.es.searchrequest.TopAggregationDefinition.NON_STICKY;
@@ -145,6 +148,7 @@ import static org.sonar.server.issue.index.IssueIndex.Facet.IMPACT_SOFTWARE_QUAL
import static org.sonar.server.issue.index.IssueIndex.Facet.ISSUE_STATUSES;
import static org.sonar.server.issue.index.IssueIndex.Facet.LANGUAGES;
import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_ASVS_40;
+import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_MOBILE_TOP_10_2024;
import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_TOP_10;
import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_TOP_10_2021;
import static org.sonar.server.issue.index.IssueIndex.Facet.PCI_DSS_32;
@@ -185,6 +189,7 @@ import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_LINE
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_NEW_CODE_REFERENCE;
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_NEW_STATUS;
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_ASVS_40;
+import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024;
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10;
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10_2021;
import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_PCI_DSS_32;
@@ -224,6 +229,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IMPACT_SOFT
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE_STATUSES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_ASVS_40;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_MOBILE_TOP_10_2024;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10_2021;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PCI_DSS_32;
@@ -256,7 +262,6 @@ public class IssueIndex {
private static final String ISSUES_WITH_SECURITY_IMPACT = "issues_with_security_impact";
private static final String AGG_IMPACT_SEVERITIES = "impact_severities";
private static final String AGG_TO_REVIEW_SECURITY_HOTSPOTS = "toReviewSecurityHotspots";
- private static final String AGG_IN_REVIEW_SECURITY_HOTSPOTS = "inReviewSecurityHotspots";
private static final String AGG_REVIEWED_SECURITY_HOTSPOTS = "reviewedSecurityHotspots";
private static final String AGG_DISTRIBUTION = "distribution";
private static final BoolQueryBuilder NON_RESOLVED_VULNERABILITIES_FILTER = boolQuery()
@@ -266,10 +271,6 @@ public class IssueIndex {
.filter(nestedQuery(FIELD_ISSUE_IMPACTS, termsQuery(FIELD_ISSUE_IMPACT_SOFTWARE_QUALITY, SoftwareQuality.SECURITY.name()),
ScoreMode.Avg))
.mustNot(existsQuery(FIELD_ISSUE_RESOLUTION));
- private static final BoolQueryBuilder IN_REVIEW_HOTSPOTS_FILTER = boolQuery()
- .filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name()))
- .filter(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_IN_REVIEW))
- .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION));
private static final BoolQueryBuilder TO_REVIEW_HOTSPOTS_FILTER = boolQuery()
.filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name()))
.filter(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_TO_REVIEW))
@@ -313,6 +314,7 @@ public class IssueIndex {
PCI_DSS_32(PARAM_PCI_DSS_32, FIELD_ISSUE_PCI_DSS_32, STICKY, DEFAULT_FACET_SIZE),
PCI_DSS_40(PARAM_PCI_DSS_40, FIELD_ISSUE_PCI_DSS_40, STICKY, DEFAULT_FACET_SIZE),
OWASP_ASVS_40(PARAM_OWASP_ASVS_40, FIELD_ISSUE_OWASP_ASVS_40, STICKY, DEFAULT_FACET_SIZE),
+ OWASP_MOBILE_TOP_10_2024(PARAM_OWASP_MOBILE_TOP_10_2024, FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024, STICKY, DEFAULT_FACET_SIZE),
OWASP_TOP_10(PARAM_OWASP_TOP_10, FIELD_ISSUE_OWASP_TOP_10, STICKY, DEFAULT_FACET_SIZE),
OWASP_TOP_10_2021(PARAM_OWASP_TOP_10_2021, FIELD_ISSUE_OWASP_TOP_10_2021, STICKY, DEFAULT_FACET_SIZE),
STIG_ASD_V5R3(PARAM_STIG_ASD_V5R3, FIELD_ISSUE_STIG_ASD_V5R3, STICKY, DEFAULT_FACET_SIZE),
@@ -531,6 +533,7 @@ public class IssueIndex {
addSecurityCategoryPrefixFilter(FIELD_ISSUE_PCI_DSS_32, PCI_DSS_32, query.pciDss32(), filters);
addSecurityCategoryPrefixFilter(FIELD_ISSUE_PCI_DSS_40, PCI_DSS_40, query.pciDss40(), filters);
addOwaspAsvsFilter(FIELD_ISSUE_OWASP_ASVS_40, OWASP_ASVS_40, query, filters);
+ addSecurityCategoryFilter(FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024, OWASP_MOBILE_TOP_10_2024, query.owaspMobileTop10For2024(), filters);
addSecurityCategoryFilter(FIELD_ISSUE_OWASP_TOP_10, OWASP_TOP_10, query.owaspTop10(), filters);
addSecurityCategoryFilter(FIELD_ISSUE_OWASP_TOP_10_2021, OWASP_TOP_10_2021, query.owaspTop10For2021(), filters);
addSecurityCategoryFilter(FIELD_ISSUE_STIG_ASD_V5R3, STIG_ASD_V5R3, query.stigAsdV5R3(), filters);
@@ -543,9 +546,8 @@ public class IssueIndex {
addImpactFilters(query, filters);
addComponentRelatedFilters(query, filters);
addDatesFilter(filters, query);
- addCreatedAfterByProjectsFilter(filters, query);
+ addNewCodeByProjectsFilter(filters, query);
addNewCodeReferenceFilter(filters, query);
- addNewCodeReferenceFilterByProjectsFilter(filters, query);
return filters;
}
@@ -873,29 +875,19 @@ public class IssueIndex {
}
}
- private static void addNewCodeReferenceFilterByProjectsFilter(AllFilters allFilters, IssueQuery query) {
- Collection<String> newCodeOnReferenceByProjectUuids = query.newCodeOnReferenceByProjectUuids();
- BoolQueryBuilder boolQueryBuilder = boolQuery();
-
- if (!newCodeOnReferenceByProjectUuids.isEmpty()) {
-
- newCodeOnReferenceByProjectUuids.forEach(projectOrProjectBranchUuid -> boolQueryBuilder.should(boolQuery()
- .filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectOrProjectBranchUuid))
- .filter(termQuery(FIELD_ISSUE_NEW_CODE_REFERENCE, true))));
-
- allFilters.addFilter("__is_new_code_reference_by_project_uuids",
- new SimpleFieldFilterScope("newCodeReferenceByProjectUuids"), boolQueryBuilder);
- }
- }
-
- private static void addCreatedAfterByProjectsFilter(AllFilters allFilters, IssueQuery query) {
+ private static void addNewCodeByProjectsFilter(AllFilters allFilters, IssueQuery query) {
Map<String, PeriodStart> createdAfterByProjectUuids = query.createdAfterByProjectUuids();
BoolQueryBuilder boolQueryBuilder = boolQuery();
createdAfterByProjectUuids.forEach((projectOrProjectBranchUuid, createdAfterDate) -> boolQueryBuilder.should(boolQuery()
.filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectOrProjectBranchUuid))
.filter(rangeQuery(FIELD_ISSUE_FUNC_CREATED_AT).from(createdAfterDate.date().getTime(), createdAfterDate.inclusive()))));
- allFilters.addFilter("__created_after_by_project_uuids", new SimpleFieldFilterScope("createdAfterByProjectUuids"), boolQueryBuilder);
+ Collection<String> newCodeOnReferenceByProjectUuids = query.newCodeOnReferenceByProjectUuids();
+ newCodeOnReferenceByProjectUuids.forEach(projectOrProjectBranchUuid -> boolQueryBuilder.should(boolQuery()
+ .filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectOrProjectBranchUuid))
+ .filter(termQuery(FIELD_ISSUE_NEW_CODE_REFERENCE, true))));
+
+ allFilters.addFilter("__new_code_by_project_uuids", new SimpleFieldFilterScope("newCodeByProjectUuids"), boolQueryBuilder);
}
private void validateCreationDateBounds(@Nullable Date createdBefore, @Nullable Date createdAfter) {
@@ -925,6 +917,7 @@ public class IssueIndex {
addSecurityCategoryFacetIfNeeded(PARAM_PCI_DSS_32, PCI_DSS_32, options, aggregationHelper, esRequest, query.pciDss32().toArray());
addSecurityCategoryFacetIfNeeded(PARAM_PCI_DSS_40, PCI_DSS_40, options, aggregationHelper, esRequest, query.pciDss40().toArray());
addSecurityCategoryFacetIfNeeded(PARAM_OWASP_ASVS_40, OWASP_ASVS_40, options, aggregationHelper, esRequest, query.owaspAsvs40().toArray());
+ addSecurityCategoryFacetIfNeeded(PARAM_OWASP_MOBILE_TOP_10_2024, OWASP_MOBILE_TOP_10_2024, options, aggregationHelper, esRequest, query.owaspMobileTop10For2024().toArray());
addSecurityCategoryFacetIfNeeded(PARAM_OWASP_TOP_10, OWASP_TOP_10, options, aggregationHelper, esRequest, query.owaspTop10().toArray());
addSecurityCategoryFacetIfNeeded(PARAM_OWASP_TOP_10_2021, OWASP_TOP_10_2021, options, aggregationHelper, esRequest, query.owaspTop10For2021().toArray());
addSecurityCategoryFacetIfNeeded(PARAM_STIG_ASD_V5R3, STIG_ASD_V5R3, options, aggregationHelper, esRequest, query.stigAsdV5R3().toArray());
@@ -1301,7 +1294,7 @@ public class IssueIndex {
}
private static SecurityStandardCategoryStatistics emptyCweStatistics(String rule) {
- return new SecurityStandardCategoryStatistics(rule, 0, OptionalInt.of(1), 0, 0, 1, null, null);
+ return new SecurityStandardCategoryStatistics(rule, 0, OptionalInt.of(1), 0, 0, 1, null, null, Map.of());
}
public List<SecurityStandardCategoryStatistics> getSonarSourceReport(String projectUuid, boolean isViewOrApp, boolean includeCwe) {
@@ -1347,6 +1340,17 @@ public class IssueIndex {
return searchWithLevelDistribution(request, version.label(), Integer.toString(level));
}
+ public List<SecurityStandardCategoryStatistics> getOwaspMobileTop10Report(String projectUuid, boolean isViewOrApp, boolean includeCwe, OwaspMobileTop10Version version) {
+ SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
+ IntStream.rangeClosed(1, 10).mapToObj(i -> "m" + i)
+ .forEach(owaspMobileCategory -> request.aggregation(
+ newSecurityReportSubAggregations(
+ AggregationBuilders.filter(owaspMobileCategory, boolQuery().filter(termQuery(version.prefix(), owaspMobileCategory))),
+ includeCwe,
+ null)));
+ return search(request, includeCwe, version.label());
+ }
+
public List<SecurityStandardCategoryStatistics> getOwaspTop10Report(String projectUuid, boolean isViewOrApp, boolean includeCwe, OwaspTop10Version version) {
SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp);
IntStream.rangeClosed(1, 10).mapToObj(i -> "a" + i)
@@ -1446,10 +1450,11 @@ public class IssueIndex {
Aggregation severitiesAggregations =
((ParsedFilter) categoryBucket.getAggregations().get(AGG_VULNERABILITIES)).getAggregations().get(AGG_SEVERITIES);
- CountAndRating countAndRating = getCountAndRating(severitiesAggregations);
- long vulnerabilities = countAndRating.getCount();
+ SeverityAggregationDetails severityAggregationDetails = getSeverityDetails(severitiesAggregations);
+ long vulnerabilities = severityAggregationDetails.getCount();
// Worst severity having at least one issue
- OptionalInt severityRating = countAndRating.getRating();
+ OptionalInt severityRating = severityAggregationDetails.getRating();
+ Map<String, Long> severityDistribution = severityAggregationDetails.getDistribution();
long toReviewSecurityHotspots = ((ParsedValueCount) ((ParsedFilter) categoryBucket.getAggregations().get(AGG_TO_REVIEW_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT))
.getValue();
@@ -1460,32 +1465,39 @@ public class IssueIndex {
Integer securityReviewRating = computeRating(percent.orElse(null)).getIndex();
return new SecurityStandardCategoryStatistics(categoryName, vulnerabilities, severityRating, toReviewSecurityHotspots,
- reviewedSecurityHotspots, securityReviewRating, children, version);
+ reviewedSecurityHotspots, securityReviewRating, children, version, severityDistribution);
}
- private CountAndRating getCountAndRating(Aggregation severitiesAggregations) {
+ private SeverityAggregationDetails getSeverityDetails(Aggregation severitiesAggregations) {
+ List<? extends Terms.Bucket> severityBuckets;
+ long vulnerabilities;
+ OptionalInt severityRating;
if (isMQRMode()) {
- List<? extends Terms.Bucket> severityBuckets =
+ severityBuckets =
((ParsedStringTerms) ((ParsedFilter) ((ParsedNested) severitiesAggregations).getAggregations().get(ISSUES_WITH_SECURITY_IMPACT)).getAggregations().get(AGG_IMPACT_SEVERITIES)).getBuckets();
- long vulnerabilities =
+ vulnerabilities =
severityBuckets.stream().mapToLong(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue()).sum();
// Worst severity having at least one issue
- OptionalInt severityRating = severityBuckets.stream()
+ severityRating = severityBuckets.stream()
.filter(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue() != 0)
.mapToInt(b -> org.sonar.api.issue.impact.Severity.valueOf(b.getKeyAsString()).ordinal() + 1)
.max();
- return new CountAndRating(vulnerabilities, severityRating);
} else {
- List<? extends Terms.Bucket> severityBuckets = ((ParsedStringTerms) severitiesAggregations).getBuckets();
- long vulnerabilities =
+ severityBuckets = ((ParsedStringTerms) severitiesAggregations).getBuckets();
+ vulnerabilities =
severityBuckets.stream().mapToLong(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue()).sum();
// Worst severity having at least one issue
- OptionalInt severityRating = severityBuckets.stream()
+ severityRating = severityBuckets.stream()
.filter(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue() != 0)
.mapToInt(b -> Severity.ALL.indexOf(b.getKeyAsString()) + 1)
.max();
- return new CountAndRating(vulnerabilities, severityRating);
}
+ Map<String, Long> severityDistribution = severityBuckets.stream()
+ .collect(Collectors.toMap(
+ e -> e.getKeyAsString().toLowerCase(Locale.US),
+ MultiBucketsAggregation.Bucket::getDocCount
+ ));
+ return new SeverityAggregationDetails(vulnerabilities, severityRating, severityDistribution);
}
private AggregationBuilder newSecurityReportSubAggregations(AggregationBuilder categoriesAggs, String securityStandardVersionPrefix) {
@@ -1523,9 +1535,6 @@ public class IssueIndex {
.subAggregation(AggregationBuilders.filter(AGG_TO_REVIEW_SECURITY_HOTSPOTS, TO_REVIEW_HOTSPOTS_FILTER)
.subAggregation(
AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)))
- .subAggregation(AggregationBuilders.filter(AGG_IN_REVIEW_SECURITY_HOTSPOTS, IN_REVIEW_HOTSPOTS_FILTER)
- .subAggregation(
- AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)))
.subAggregation(AggregationBuilders.filter(AGG_REVIEWED_SECURITY_HOTSPOTS, REVIEWED_HOTSPOTS_FILTER)
.subAggregation(
AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY)));
@@ -1569,7 +1578,6 @@ public class IssueIndex {
componentFilter
.should(getNonResolvedIssuesOrNonResolvedSecurityImpactQueryBuilderBasedOnMode())
.should(TO_REVIEW_HOTSPOTS_FILTER)
- .should(IN_REVIEW_HOTSPOTS_FILTER)
.should(REVIEWED_HOTSPOTS_FILTER)
.minimumShouldMatch(1))
.size(0);
@@ -1580,13 +1588,15 @@ public class IssueIndex {
}
- private static class CountAndRating {
+ private static class SeverityAggregationDetails {
private long count;
private OptionalInt rating;
+ private Map<String, Long> distribution;
- public CountAndRating(long count, OptionalInt rating) {
+ public SeverityAggregationDetails(long count, OptionalInt rating, Map<String, Long> distribution) {
this.count = count;
this.rating = rating;
+ this.distribution = distribution;
}
public long getCount() {
@@ -1596,5 +1606,9 @@ public class IssueIndex {
public OptionalInt getRating() {
return rating;
}
+
+ public Map<String, Long> getDistribution() {
+ return distribution;
+ }
}
}
diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java
index ddf06536376..9a20bac4775 100644
--- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java
+++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java
@@ -74,6 +74,7 @@ public class IssueQuery {
private final Collection<String> languages;
private final Collection<String> tags;
private final Collection<String> types;
+ private final Collection<String> owaspMobileTop10For2024;
private final Collection<String> owaspTop10;
private final Collection<String> pciDss32;
private final Collection<String> pciDss40;
@@ -129,6 +130,7 @@ public class IssueQuery {
this.pciDss40 = defaultCollection(builder.pciDss40);
this.owaspAsvs40 = defaultCollection(builder.owaspAsvs40);
this.owaspAsvsLevel = builder.owaspAsvsLevel;
+ this.owaspMobileTop10For2024 = defaultCollection(builder.owaspMobileTop10For2024);
this.owaspTop10 = defaultCollection(builder.owaspTop10);
this.owaspTop10For2021 = defaultCollection(builder.owaspTop10For2021);
this.stigAsdV5R3 = defaultCollection(builder.stigAsdV5R3);
@@ -256,6 +258,10 @@ public class IssueQuery {
return Optional.ofNullable(owaspAsvsLevel);
}
+ public Collection<String> owaspMobileTop10For2024() {
+ return owaspMobileTop10For2024;
+ }
+
public Collection<String> owaspTop10() {
return owaspTop10;
}
@@ -402,6 +408,7 @@ public class IssueQuery {
private Collection<String> pciDss40;
private Collection<String> owaspAsvs40;
private Integer owaspAsvsLevel;
+ private Collection<String> owaspMobileTop10For2024;
private Collection<String> owaspTop10;
private Collection<String> owaspTop10For2021;
private Collection<String> stigAsdV5R3;
@@ -556,6 +563,11 @@ public class IssueQuery {
return this;
}
+ public Builder owaspMobileTop10For2024(@Nullable Collection<String> o) {
+ this.owaspMobileTop10For2024 = o;
+ return this;
+ }
+
public Builder owaspTop10(@Nullable Collection<String> o) {
this.owaspTop10 = o;
return this;
diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java
index e105cd9d175..a8c4216b14f 100644
--- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java
+++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java
@@ -150,6 +150,7 @@ public class IssueQueryFactory {
.pciDss40(request.getPciDss40())
.owaspAsvs40(request.getOwaspAsvs40())
.owaspAsvsLevel(request.getOwaspAsvsLevel())
+ .owaspMobileTop10For2024(request.getOwaspMobileTop10For2024())
.owaspTop10(request.getOwaspTop10())
.owaspTop10For2021(request.getOwaspTop10For2021())
.stigAsdR5V3(request.getStigAsdV5R3())
@@ -167,6 +168,8 @@ public class IssueQueryFactory {
List<ComponentDto> allComponents = new ArrayList<>();
boolean effectiveOnComponentOnly = mergeDeprecatedComponentParameters(dbSession, request, allComponents);
addComponentParameters(builder, dbSession, effectiveOnComponentOnly, allComponents, request);
+ // SONAR-25108
+ unsetMainBranch(builder, issueKeys != null && !issueKeys.isEmpty(), allComponents, request);
setCreatedAfterFromRequest(dbSession, builder, request, allComponents, timeZone);
String sort = request.getSort();
@@ -511,4 +514,12 @@ public class IssueQueryFactory {
builder.mainBranch(branchDto.isMain());
}
}
+
+ private static void unsetMainBranch(IssueQuery.Builder builder, boolean hasIssueKey, List<ComponentDto> components, SearchRequest request) {
+ var pullRequest = request.getPullRequest();
+ var branch = request.getBranch();
+ if ((components.isEmpty() || UNKNOWN_COMPONENT.equals(components.get(0)) || (pullRequest == null && branch == null)) && hasIssueKey) {
+ builder.mainBranch(null);
+ }
+ }
}
diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java
index 23daad4fed0..e0fa9ca61cc 100644
--- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java
+++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java
@@ -456,7 +456,7 @@ class IssueIndexFiltersTest extends IssueIndexTestCommon {
}
@Test
- void filter_by_new_code_reference_branches() {
+ void filter_by_new_reference_branches() {
ComponentDto project1 = db.components().insertPrivateProject().getMainBranchComponent();
IssueDoc project1Issue1 = newDocForProject(project1).setIsNewCodeReference(true);
IssueDoc project1Issue2 = newDocForProject(project1).setIsNewCodeReference(false);
@@ -474,14 +474,20 @@ class IssueIndexFiltersTest extends IssueIndexTestCommon {
IssueDoc project2Branch1Issue1 = newDoc(project2Branch1, project2.uuid()).setIsNewCodeReference(false);
IssueDoc project2Branch1Issue2 = newDoc(project2Branch1, project2.uuid()).setIsNewCodeReference(true);
+ ComponentDto project3 = db.components().insertPrivateProject().getMainBranchComponent();
+ ComponentDto project3Branch1 = db.components().insertProjectBranch(project2);
+ IssueDoc project3Issue1 = newDoc(project3Branch1, project3.uuid()).setFuncCreationDate(new Date(1000L));
+ IssueDoc project3Issue2 = newDoc(project3Branch1, project3.uuid()).setFuncCreationDate(new Date(2000L));
+
indexIssues(project1Issue1, project1Issue2, project2Issue1, project2Issue2,
- project1Branch1Issue1, project1Branch1Issue2, project2Branch1Issue1, project2Branch1Issue2);
+ project1Branch1Issue1, project1Branch1Issue2, project2Branch1Issue1, project2Branch1Issue2, project3Issue1, project3Issue2);
// Search for issues of project 1 branch 1 and project 2 branch 1 that are new code on a branch using reference for new code
assertThatSearchReturnsOnly(IssueQuery.builder()
.mainBranch(false)
- .newCodeOnReferenceByProjectUuids(Set.of(project1Branch1.uuid(), project2Branch1.uuid())),
- project1Branch1Issue2.key(), project2Branch1Issue2.key());
+ .newCodeOnReferenceByProjectUuids(Set.of(project1Branch1.uuid(), project2Branch1.uuid()))
+ .createdAfterByProjectUuids(Map.of(project3Branch1.uuid(), new IssueQuery.PeriodStart(new Date(1500), false))),
+ project1Branch1Issue2.key(), project2Branch1Issue2.key(), project3Issue2.key());
}
@Test
diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java
index 0eee21734c2..0c002a4e13c 100644
--- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java
+++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java
@@ -30,7 +30,9 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.IssueStatus;
import org.sonar.api.rule.Severity;
+import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.core.rule.RuleType;
import org.sonar.api.server.rule.RulesDefinition.StigVersion;
import org.sonar.db.component.ComponentDto;
@@ -51,6 +53,7 @@ import static org.sonar.api.issue.impact.Severity.MEDIUM;
import static org.sonar.api.issue.impact.SoftwareQuality.MAINTAINABILITY;
import static org.sonar.api.issue.impact.SoftwareQuality.SECURITY;
import static org.sonar.api.server.rule.RulesDefinition.OwaspAsvsVersion;
+import static org.sonar.api.server.rule.RulesDefinition.OwaspMobileTop10Version.Y2024;
import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2017;
import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2021;
import static org.sonar.api.server.rule.RulesDefinition.PciDssVersion;
@@ -357,6 +360,31 @@ class IssueIndexSecurityReportsTest extends IssueIndexTestCommon {
tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 5));
}
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ void getOwaspMobileTop10For2024Report_aggregation_with_cwe(boolean mqrMode) {
+ doReturn(Optional.of(mqrMode)).when(config).getBoolean(MULTI_QUALITY_MODE_ENABLED);
+ List<SecurityStandardCategoryStatistics> owaspTop10Report = indexIssuesAndAssertOwaspMobile2024Report(true);
+
+ Map<String, List<SecurityStandardCategoryStatistics>> cweByOwasp = owaspTop10Report.stream()
+ .collect(Collectors.toMap(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getChildren));
+
+ assertThat(cweByOwasp.get("m1")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+ SecurityStandardCategoryStatistics::getVulnerabilityRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots,
+ SecurityStandardCategoryStatistics::getReviewedSecurityHotspots, SecurityStandardCategoryStatistics::getSecurityReviewRating)
+ .containsExactlyInAnyOrder(
+ tuple("123", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1),
+ tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1),
+ tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 5));
+ assertThat(cweByOwasp.get("m3")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+ SecurityStandardCategoryStatistics::getVulnerabilityRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots,
+ SecurityStandardCategoryStatistics::getReviewedSecurityHotspots, SecurityStandardCategoryStatistics::getSecurityReviewRating)
+ .containsExactlyInAnyOrder(
+ tuple("123", 2L /* openvul1, openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1),
+ tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1),
+ tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 5));
+ }
+
private List<SecurityStandardCategoryStatistics> indexIssuesAndAssertOwaspReport(boolean includeCwe) {
ComponentDto project = newPrivateProjectDto();
indexIssues(
@@ -545,6 +573,41 @@ class IssueIndexSecurityReportsTest extends IssueIndexTestCommon {
return owaspTop10Report;
}
+ private List<SecurityStandardCategoryStatistics> indexIssuesAndAssertOwaspMobile2024Report(boolean includeCwe) {
+ ComponentDto project = newPrivateProjectDto();
+ indexIssues(
+ newDocForProject("openvul1", project).setOwaspMobileTop10For2024(asList("m1", "m3")).setCwe(asList("123", "456")).setType(RuleType.VULNERABILITY).setImpacts(Map.of(SECURITY, MEDIUM)).setStatus(IssueStatus.OPEN.name())
+ .setSeverity(Severity.MAJOR),
+ newDocForProject("openvul2", project).setOwaspMobileTop10For2024(asList("m3", "m6")).setCwe(List.of("123")).setType(RuleType.VULNERABILITY).setImpacts(Map.of(SECURITY, LOW)).setStatus(IssueStatus.OPEN.name())
+ .setSeverity(Severity.MINOR),
+ newDocForProject("notowaspvul", project).setOwaspMobileTop10For2024(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setImpacts(Map.of(SECURITY, HIGH)).setStatus(IssueStatus.OPEN.toString())
+ .setSeverity(Severity.CRITICAL),
+ newDocForProject("toreviewhotspot1", project).setOwaspMobileTop10For2024(asList("m1", "m3")).setCwe(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT)
+ .setStatus(Issue.STATUS_TO_REVIEW),
+ newDocForProject("toreviewhotspot2", project).setOwaspMobileTop10For2024(asList("m3", "m6")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW),
+ newDocForProject("reviewedHotspot", project).setOwaspMobileTop10For2024(asList("m3", "m8")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED)
+ .setResolution(Issue.RESOLUTION_FIXED),
+ newDocForProject("notowasphotspot", project).setOwaspMobileTop10For2024(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW));
+
+ List<SecurityStandardCategoryStatistics> owaspTop10Report = underTest.getOwaspMobileTop10Report(project.uuid(), false, includeCwe, Y2024);
+ assertThat(owaspTop10Report)
+ .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities,
+ SecurityStandardCategoryStatistics::getVulnerabilityRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots,
+ SecurityStandardCategoryStatistics::getReviewedSecurityHotspots, SecurityStandardCategoryStatistics::getSecurityReviewRating)
+ .containsExactlyInAnyOrder(
+ tuple("m1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* toreviewhotspot1 */, 0L, 5),
+ tuple("m2", 0L, OptionalInt.empty(), 0L, 0L, 1),
+ tuple("m3", 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* toreviewhotspot1,toreviewhotspot2 */, 1L /* reviewedHotspot */, 4),
+ tuple("m4", 0L, OptionalInt.empty(), 0L, 0L, 1),
+ tuple("m5", 0L, OptionalInt.empty(), 0L, 0L, 1),
+ tuple("m6", 1L /* openvul2 */, OptionalInt.of(2) /* MINOR = B */, 1L /* toreviewhotspot2 */, 0L, 5),
+ tuple("m7", 0L, OptionalInt.empty(), 0L, 0L, 1),
+ tuple("m8", 0L, OptionalInt.empty(), 0L, 1L /* reviewedHotspot */, 1),
+ tuple("m9", 0L, OptionalInt.empty(), 0L, 0L, 1),
+ tuple("m10", 0L, OptionalInt.empty(), 0L, 0L, 1));
+ return owaspTop10Report;
+ }
+
@ParameterizedTest
@ValueSource(booleans = {true, false})
void getPciDssReport_aggregation_on_portfolio(boolean mqrMode) {
diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java
index eb5c05b4ab6..c62293eb309 100644
--- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java
+++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java
@@ -25,6 +25,7 @@ import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
+import java.util.List;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
@@ -741,4 +742,84 @@ public class IssueQueryFactoryTest {
.hasMessageContaining("'unknown-date' cannot be parsed as either a date or date+time");
}
+ @Test
+ public void when_issue_keys_provided_with_no_component_should_not_have_main_branch() {
+ SearchRequest request = new SearchRequest()
+ .setIssues(List.of("issue-key-1", "issue-key-2"));
+
+ IssueQuery query = underTest.create(request);
+
+ assertThat(query.isMainBranch()).isNull();
+ }
+
+ @Test
+ public void when_issue_keys_and_component_provided_should_have_main_branch_set() {
+ // Create a project with main branch
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto mainBranch = projectData.getMainBranchComponent();
+ String branchName = DEFAULT_MAIN_BRANCH_NAME;
+
+ // Request with issue keys and main branch
+ SearchRequest request = new SearchRequest()
+ .setIssues(List.of("issue-key-1", "issue-key-2"))
+ .setComponentKeys(List.of(mainBranch.getKey()))
+ .setBranch(branchName);
+
+ IssueQuery query = underTest.create(request);
+
+ // Should unset main branch since issue keys are provided
+ assertThat(query.isMainBranch()).isTrue();
+ assertThat(query.branchUuid()).isEqualTo(mainBranch.uuid());
+ }
+
+ @Test
+ public void when_no_issue_keys_provided_should_default_to_main_branch() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto mainBranch = projectData.getMainBranchComponent();
+ String branchName = DEFAULT_MAIN_BRANCH_NAME;
+
+ SearchRequest request = new SearchRequest()
+ .setComponentKeys(List.of(mainBranch.getKey()))
+ .setBranch(branchName);
+
+ IssueQuery query = underTest.create(request);
+
+ assertThat(query.isMainBranch()).isTrue();
+ assertThat(query.branchUuid()).isEqualTo(mainBranch.uuid());
+ }
+
+ @Test
+ public void when_component_is_non_main_branch_should_not_default_to_main_branch() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto mainBranch = projectData.getMainBranchComponent();
+ String branchName = "feature-branch";
+ ComponentDto branch = db.components().insertProjectBranch(mainBranch, b -> b.setKey(branchName));
+
+ SearchRequest request = new SearchRequest()
+ .setComponentKeys(List.of(branch.getKey()))
+ .setBranch(branchName);
+
+ IssueQuery query = underTest.create(request);
+
+ assertThat(query.isMainBranch()).isFalse();
+ assertThat(query.branchUuid()).isEqualTo(branch.uuid());
+ }
+
+ @Test
+ public void when_empty_issue_keys_list_provided_should_default_to_main_branch() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto mainBranch = projectData.getMainBranchComponent();
+ String branchName = DEFAULT_MAIN_BRANCH_NAME;
+
+ SearchRequest request = new SearchRequest()
+ .setIssues(Collections.emptyList())
+ .setComponentKeys(List.of(mainBranch.getKey()))
+ .setBranch(branchName);
+
+ IssueQuery query = underTest.create(request);
+
+ assertThat(query.isMainBranch()).isTrue();
+ assertThat(query.branchUuid()).isEqualTo(mainBranch.uuid());
+ }
+
}
diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java
index c8b9870d547..979e493da47 100644
--- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java
+++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java
@@ -128,6 +128,15 @@ class IssueQueryTest {
}
@Test
+ void build_owasp_mobile_query() {
+ IssueQuery query = IssueQuery.builder()
+ .owaspMobileTop10For2024(List.of("m5", "m6"))
+ .build();
+
+ assertThat(query.owaspMobileTop10For2024()).containsOnly("m5", "m6");
+ }
+
+ @Test
void build_stig_query() {
IssueQuery query = IssueQuery.builder()
.stigAsdR5V3(List.of("V-222400", "V-222401"))
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java
index 1b5b17e5e6a..9727e3b7d04 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/DefaultGroupController.java
@@ -20,6 +20,7 @@
package org.sonar.server.v2.api.group.controller;
import java.util.List;
+import org.jetbrains.annotations.Nullable;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.server.common.SearchResults;
@@ -53,10 +54,11 @@ public class DefaultGroupController implements GroupController {
}
@Override
- public GroupsSearchRestResponse search(GroupsSearchRestRequest groupsSearchRestRequest, RestPage restPage) {
+ public GroupsSearchRestResponse search(GroupsSearchRestRequest groupsSearchRestRequest, @Nullable String excludedUserId, RestPage restPage) {
userSession.checkLoggedIn().checkIsSystemAdministrator();
try (DbSession dbSession = dbClient.openSession(false)) {
- GroupSearchRequest groupSearchRequest = new GroupSearchRequest(groupsSearchRestRequest.q(), groupsSearchRestRequest.managed(), restPage.pageIndex(), restPage.pageSize());
+ GroupSearchRequest groupSearchRequest = new GroupSearchRequest(groupsSearchRestRequest.q(), groupsSearchRestRequest.managed(), groupsSearchRestRequest.userId(),
+ excludedUserId, restPage.pageIndex(), restPage.pageSize());
SearchResults<GroupInformation> searchResults = groupService.search(dbSession, groupSearchRequest);
List<GroupRestResponse> groupRestResponses = toGroupRestResponses(searchResults);
return new GroupsSearchRestResponse(groupRestResponses, new PageRestResponse(restPage.pageIndex(), restPage.pageSize(), searchResults.total()));
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java
index 666ba5e049e..f6f6a149679 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/controller/GroupController.java
@@ -22,7 +22,11 @@ package org.sonar.server.v2.api.group.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
+import javax.annotation.Nullable;
import org.sonar.server.v2.api.group.request.GroupCreateRestRequest;
import org.sonar.server.v2.api.group.request.GroupUpdateRestRequest;
import org.sonar.server.v2.api.group.request.GroupsSearchRestRequest;
@@ -39,6 +43,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@@ -57,6 +62,8 @@ public interface GroupController {
""")
GroupsSearchRestResponse search(
@Valid @ParameterObject GroupsSearchRestRequest groupsSearchRestRequest,
+ @RequestParam(name = "userId!") @Nullable @Schema(description = "Find groups without this user. Only available for system administrators.",
+ extensions = @Extension(properties = {@ExtensionProperty(name = "internal", value = "true")}), hidden = true) String excludedUserId,
@Valid @ParameterObject RestPage restPage);
@GetMapping(path = "/{id}")
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/request/GroupsSearchRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/request/GroupsSearchRestRequest.java
index ab704c8112b..d829f29716b 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/request/GroupsSearchRestRequest.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/group/request/GroupsSearchRestRequest.java
@@ -19,6 +19,8 @@
*/
package org.sonar.server.v2.api.group.request;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.annotation.Nullable;
@@ -30,7 +32,12 @@ public record GroupsSearchRestRequest(
@Nullable
@Schema(description = "Filter on name.\n"
+ "This parameter performs a partial match (contains), it is case insensitive.")
- String q
+ String q,
+
+ @Nullable
+ @Schema(description = "Filter groups containing the user. Only available for system administrators. Using != operator will search for groups without the user.",
+ extensions = @Extension(properties = {@ExtensionProperty(name = "internal", value = "true")}))
+ String userId
) {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java
index 3ea79cb5467..19677708455 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java
@@ -21,6 +21,7 @@ package org.sonar.server.v2.api.projectbindings.controller;
import java.util.List;
import java.util.Optional;
+import org.apache.commons.lang3.StringUtils;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.server.common.SearchResults;
@@ -35,8 +36,9 @@ import org.sonar.server.v2.api.projectbindings.request.ProjectBindingsSearchRest
import org.sonar.server.v2.api.projectbindings.response.ProjectBindingsSearchRestResponse;
import org.sonar.server.v2.api.response.PageRestResponse;
-import static org.sonar.db.permission.ProjectPermission.USER;
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+import static org.sonar.db.permission.ProjectPermission.USER;
+import static org.sonar.server.exceptions.BadRequestException.throwBadRequestException;
public class DefaultProjectBindingsController implements ProjectBindingsController {
@@ -61,25 +63,26 @@ public class DefaultProjectBindingsController implements ProjectBindingsControll
}
}
- private static ProjectBinding toProjectBinding(ProjectDto projectDto, ProjectAlmSettingDto projectAlmSettingDto) {
- return new ProjectBinding(
- projectAlmSettingDto.getUuid(),
- projectAlmSettingDto.getAlmSettingUuid(),
- projectAlmSettingDto.getProjectUuid(),
- projectDto.getKey(),
- projectAlmSettingDto.getAlmRepo(),
- projectAlmSettingDto.getAlmSlug());
- }
-
@Override
public ProjectBindingsSearchRestResponse searchProjectBindings(ProjectBindingsSearchRestRequest restRequest, RestPage restPage) {
userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS);
- ProjectBindingsSearchRequest serviceRequest = new ProjectBindingsSearchRequest(restRequest.repository(), restRequest.dopSettingId(), restPage.pageIndex(), restPage.pageSize());
+ validateSearchParameters(restRequest);
+ ProjectBindingsSearchRequest serviceRequest = new ProjectBindingsSearchRequest(
+ restRequest.repository(), restRequest.dopSettingId(), restRequest.repositoryUrl(), restPage.pageIndex(), restPage.pageSize());
SearchResults<ProjectBindingInformation> searchResults = projectBindingsService.findProjectBindingsByRequest(serviceRequest);
List<ProjectBinding> projectBindings = toProjectBindings(searchResults);
return new ProjectBindingsSearchRestResponse(projectBindings, new PageRestResponse(restPage.pageIndex(), restPage.pageSize(), searchResults.total()));
}
+ private static void validateSearchParameters(ProjectBindingsSearchRestRequest request) {
+ boolean hasRepositoryUrl = StringUtils.isNotBlank(request.repositoryUrl());
+ boolean hasOtherParams = StringUtils.isNotBlank(request.repository()) || StringUtils.isNotBlank(request.dopSettingId());
+
+ if (hasRepositoryUrl && hasOtherParams) {
+ throwBadRequestException("'repositoryUrl' parameter cannot be used together with 'repository' or 'dopSettingId' parameters");
+ }
+ }
+
private static List<ProjectBinding> toProjectBindings(SearchResults<ProjectBindingInformation> searchResults) {
return searchResults.searchResults().stream()
.map(DefaultProjectBindingsController::toProjectBinding)
@@ -95,4 +98,15 @@ public class DefaultProjectBindingsController implements ProjectBindingsControll
projectBindingInformation.repository(),
projectBindingInformation.slug());
}
+
+ private static ProjectBinding toProjectBinding(ProjectDto projectDto, ProjectAlmSettingDto projectAlmSettingDto) {
+ return new ProjectBinding(
+ projectAlmSettingDto.getUuid(),
+ projectAlmSettingDto.getAlmSettingUuid(),
+ projectAlmSettingDto.getProjectUuid(),
+ projectDto.getKey(),
+ projectAlmSettingDto.getAlmRepo(),
+ projectAlmSettingDto.getAlmSlug());
+ }
+
}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java
index 6df2d7980f7..c3020fd06cc 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java
@@ -22,19 +22,21 @@ package org.sonar.server.v2.api.projectbindings.request;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.annotation.Nullable;
-public record ProjectBindingsSearchRestRequest (
+public record ProjectBindingsSearchRestRequest(
- @Nullable
- @Schema(
+ @Nullable @Schema(
description = """
Filter on the repository name.
This parameter performs an exact, case insensitive, match.
- """)
- String repository,
+ """) String repository,
- @Nullable
- @Schema(description = "Filter on the DevOps Platform setting id.")
- String dopSettingId
+ @Nullable @Schema(description = "Filter on the DevOps Platform setting id.") String dopSettingId,
+
+ @Nullable @Schema(
+ description = """
+ Filter on the repository URL.
+ This parameter can be in different formats, the traditional URL or the git remote URL (https or ssh).
+ """) String repositoryUrl
) {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java
index 8cf5bc05bb1..a158f205091 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java
@@ -64,7 +64,7 @@ public class UsersSearchRestResponseGenerator implements UsersSearchResponseGene
String login = userDto.getLogin();
String name = userDto.getName();
if (!userSession.isLoggedIn()) {
- return new UserRestResponseForAnonymousUsers(id, login, name);
+ return new UserRestResponseForAnonymousUsers(login, name);
}
String avatar = userInformation.avatar().orElse(null);
@@ -95,7 +95,7 @@ public class UsersSearchRestResponseGenerator implements UsersSearchResponseGene
slLastConnectionDate,
scmAccounts);
}
- return new UserRestResponseForLoggedInUsers(id, login, name, email, active, local, externalIdentityProvider, avatar);
+ return new UserRestResponseForLoggedInUsers(login, name, active, avatar);
}
private static String toDateTime(@Nullable Long dateTimeMs) {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForAnonymousUsers.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForAnonymousUsers.java
index 71c344b814b..c6d787be217 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForAnonymousUsers.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForAnonymousUsers.java
@@ -20,7 +20,6 @@
package org.sonar.server.v2.api.user.response;
public record UserRestResponseForAnonymousUsers(
- String id,
String login,
String name
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForLoggedInUsers.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForLoggedInUsers.java
index 25900316432..4b84fbdd0de 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForLoggedInUsers.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UserRestResponseForLoggedInUsers.java
@@ -22,18 +22,11 @@ package org.sonar.server.v2.api.user.response;
import javax.annotation.Nullable;
public record UserRestResponseForLoggedInUsers(
- String id,
String login,
String name,
@Nullable
- String email,
- @Nullable
Boolean active,
@Nullable
- Boolean local,
- @Nullable
- String externalProvider,
- @Nullable
String avatar
) implements UserRestResponse {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/ErrorMessages.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/ErrorMessages.java
index 08ffe167e3b..f533c95c066 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/ErrorMessages.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/ErrorMessages.java
@@ -38,6 +38,7 @@ enum ErrorMessages {
UNACCEPTABLE_MEDIA_TYPE("The requested media type is not acceptable."),
UNSUPPORTED_MEDIA_TYPE("Unsupported media type."),
VALIDATION_ERROR("Validation error. Please check your input."),
+ TOO_MANY_REQUESTS("Too many requests. Please try again later."),
;
private final String message;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java
index 409796fbf05..3e5929f83cd 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java
@@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory;
import org.sonar.server.authentication.event.AuthenticationException;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ServerException;
+import org.sonar.server.exceptions.TooManyRequestsException;
import org.sonar.server.v2.api.model.RestError;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.http.HttpStatus;
@@ -48,6 +49,8 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchExcep
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
+import static org.springframework.http.HttpStatus.TOO_MANY_REQUESTS;
+
@RestControllerAdvice
public class RestResponseEntityExceptionHandler {
@@ -130,6 +133,12 @@ public class RestResponseEntityExceptionHandler {
return buildResponse(HttpStatus.PAYLOAD_TOO_LARGE, ErrorMessages.SIZE_EXCEEDED);
}
+ @ExceptionHandler(TooManyRequestsException.class)
+ protected ResponseEntity<RestError> handleTooManyRequestsException(TooManyRequestsException ex) {
+ final String errorMessage = Optional.ofNullable(ex.getMessage()).orElse(ErrorMessages.TOO_MANY_REQUESTS.getMessage());
+ return buildResponse(TOO_MANY_REQUESTS, errorMessage);
+ }
+
// endregion client
// region security
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java
index 640de88fb8a..dba11c19740 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java
@@ -22,7 +22,6 @@ package org.sonar.server.v2.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import jakarta.validation.Validation;
-import jakarta.validation.ValidatorFactory;
import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
import org.sonar.api.internal.MetadataLoader;
import org.sonar.api.utils.System2;
@@ -35,6 +34,9 @@ import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.MediaType;
+import org.springframework.validation.Validator;
+import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
+import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -53,12 +55,26 @@ public class CommonWebConfig implements WebMvcConfigurer {
configurer.setUrlPathHelper(urlPathHelper).setUseTrailingSlashMatch(true);
}
- @Bean
- public ValidatorFactory validator() {
- return Validation.byDefaultProvider()
+ @Override
+ public Validator getValidator() {
+ // This validator gets returned from the
+ // WebMvcConfigurationSupport#mvcValidator bean factory method.
+ // We can create a new one each time here and an instance will be cached
+ // in the Spring context.
+ //
+ // One reason we override the validator is to avoid a dependency
+ // on an expression language implementation like expressly.
+ //
+ // This same validator must also be configured in ControllerTester,
+ // otherwise unit test behavior will not match production behavior.
+ //
+ // The validator errors are formatted in RestResponseEntityExceptionHandler.
+ var jakartaValidator = Validation.byDefaultProvider()
.configure()
.messageInterpolator(new ParameterMessageInterpolator())
- .buildValidatorFactory();
+ .buildValidatorFactory()
+ .getValidator();
+ return new SpringValidatorAdapter(jakartaValidator);
}
@Bean
@@ -83,4 +99,9 @@ public class CommonWebConfig implements WebMvcConfigurer {
public BeanFactoryPostProcessor beanFactoryPostProcessor1(SpringDocConfigProperties springDocConfigProperties) {
return beanFactory -> springDocConfigProperties.setDefaultProducesMediaType(MediaType.APPLICATION_JSON_VALUE);
}
+
+ @Override
+ public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
+ configurer.defaultContentType(MediaType.APPLICATION_JSON, MediaType.ALL);
+ }
}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java
index c9d7070816d..2b6fec3c337 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/group/controller/DefaultGroupControllerTest.java
@@ -412,6 +412,8 @@ public class DefaultGroupControllerTest {
assertThat(requestCaptor.getValue().page()).hasToString(DEFAULT_PAGE_INDEX);
assertThat(requestCaptor.getValue().managed()).isNull();
assertThat(requestCaptor.getValue().query()).isNull();
+ assertThat(requestCaptor.getValue().userUuid()).isNull();
+ assertThat(requestCaptor.getValue().excludedUserUuid()).isNull();
}
@Test
@@ -422,6 +424,8 @@ public class DefaultGroupControllerTest {
mockMvc.perform(get(GROUPS_ENDPOINT)
.param("managed", "true")
.param("q", "q")
+ .param("userId", "userId")
+ .param("userId!", "excludedUserId")
.param("pageSize", "100")
.param("pageIndex", "2"))
.andExpect(status().isOk());
@@ -432,6 +436,8 @@ public class DefaultGroupControllerTest {
assertThat(requestCaptor.getValue().page()).isEqualTo(2);
assertThat(requestCaptor.getValue().managed()).isTrue();
assertThat(requestCaptor.getValue().query()).isEqualTo("q");
+ assertThat(requestCaptor.getValue().userUuid()).isEqualTo("userId");
+ assertThat(requestCaptor.getValue().excludedUserUuid()).isEqualTo("excludedUserId");
}
@Test
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java
index 65ef60637d1..51014f1c901 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java
@@ -39,8 +39,8 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static org.sonar.db.permission.ProjectPermission.ADMIN;
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+import static org.sonar.db.permission.ProjectPermission.ADMIN;
import static org.sonar.server.v2.WebApiEndpoints.PROJECT_BINDINGS_ENDPOINT;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@@ -223,6 +223,85 @@ class DefaultProjectBindingsControllerTest {
"""));
}
+ @Test
+ void searchProjectBindings_whenRepositoryUrlUsed_shouldForwardRepositoryUrlParameter() throws Exception {
+ userSession.logIn().addPermission(PROVISION_PROJECTS);
+ when(projectBindingsService.findProjectBindingsByRequest(any())).thenReturn(new SearchResults<>(List.of(), 0));
+
+ mockMvc
+ .perform(get(PROJECT_BINDINGS_ENDPOINT)
+ .param("repositoryUrl", "https://github.com/org/repo")
+ .param("pageIndex", "1")
+ .param("pageSize", "50"))
+ .andExpect(status().isOk());
+
+ ArgumentCaptor<ProjectBindingsSearchRequest> requestCaptor = ArgumentCaptor.forClass(ProjectBindingsSearchRequest.class);
+ verify(projectBindingsService).findProjectBindingsByRequest(requestCaptor.capture());
+ assertThat(requestCaptor.getValue().repositoryUrl()).isEqualTo("https://github.com/org/repo");
+ assertThat(requestCaptor.getValue().repository()).isNull();
+ assertThat(requestCaptor.getValue().dopSettingId()).isNull();
+ assertThat(requestCaptor.getValue().page()).isEqualTo(1);
+ assertThat(requestCaptor.getValue().pageSize()).isEqualTo(50);
+ }
+
+ @Test
+ void searchProjectBindings_whenRepositoryUrlWithRepositoryParameter_shouldReturnBadRequest() throws Exception {
+ userSession.logIn().addPermission(PROVISION_PROJECTS);
+
+ mockMvc
+ .perform(get(PROJECT_BINDINGS_ENDPOINT)
+ .param("repositoryUrl", "https://github.com/org/repo")
+ .param("repository", "repo"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void searchProjectBindings_whenRepositoryUrlWithDopSettingIdParameter_shouldReturnBadRequest() throws Exception {
+ userSession.logIn().addPermission(PROVISION_PROJECTS);
+
+ mockMvc
+ .perform(get(PROJECT_BINDINGS_ENDPOINT)
+ .param("repositoryUrl", "https://github.com/org/repo")
+ .param("dopSettingId", "setting123"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void searchProjectBindings_whenRepositoryUrlReturnsResults_shouldReturnThem() throws Exception {
+ userSession.logIn().addPermission(PROVISION_PROJECTS);
+
+ ProjectBindingInformation dto1 = projectBindingInformation("1");
+ List<ProjectBindingInformation> expectedResults = List.of(dto1);
+ when(projectBindingsService.findProjectBindingsByRequest(any())).thenReturn(new SearchResults<>(expectedResults, expectedResults.size()));
+
+ mockMvc
+ .perform(get(PROJECT_BINDINGS_ENDPOINT)
+ .param("repositoryUrl", "https://github.com/org/repo")
+ .param("pageIndex", "1")
+ .param("pageSize", "50"))
+ .andExpectAll(
+ status().isOk(),
+ content().json("""
+ {
+ "projectBindings": [
+ {
+ "id": "uuid_1",
+ "devOpsPlatformSettingId": "almSettingUuid_1",
+ "projectId": "projectUuid_1",
+ "projectKey": "projectKey_1",
+ "repository": "almRepo_1",
+ "slug": "almSlug_1"
+ }
+ ],
+ "page": {
+ "pageIndex": 1,
+ "pageSize": 50,
+ "total": 1
+ }
+ }
+ """));
+ }
+
private static ProjectAlmSettingDto mockProjectAlmSettingDto(String i) {
ProjectAlmSettingDto dto = mock();
when(dto.getUuid()).thenReturn("uuid_" + i);
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java
index 0780e7435ab..bda36f5e806 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java
@@ -121,13 +121,9 @@ public class UsersSearchRestResponseGeneratorTest {
private static UserRestResponseForLoggedInUsers buildExpectedResponseForUser(UserInformation userInformation) {
UserDto userDto = userInformation.userDto();
return new UserRestResponseForLoggedInUsers(
- userDto.getUuid(),
userDto.getLogin(),
userDto.getName(),
- userDto.getEmail(),
userDto.isActive(),
- userDto.isLocal(),
- userDto.getExternalIdentityProvider(),
userInformation.avatar().orElse(null)
);
}
@@ -150,7 +146,6 @@ public class UsersSearchRestResponseGeneratorTest {
private static UserRestResponseForAnonymousUsers buildExpectedResponseForAnonymous(UserInformation userInformation) {
UserDto userDto = userInformation.userDto();
return new UserRestResponseForAnonymousUsers(
- userDto.getUuid(),
userDto.getLogin(),
userDto.getName()
);
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java
index 7dbe13a3a18..2db9c240baf 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java
@@ -40,6 +40,7 @@ import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.exceptions.ServerException;
import org.sonar.server.exceptions.TemplateMatchingKeyException;
+import org.sonar.server.exceptions.TooManyRequestsException;
import org.sonar.server.exceptions.UnauthorizedException;
import org.sonar.server.v2.api.model.RestError;
import org.springframework.core.MethodParameter;
@@ -325,6 +326,16 @@ class RestResponseEntityExceptionHandlerTest {
assertThat(logs.logs(Level.WARN)).contains(ErrorMessages.SIZE_EXCEEDED.getMessage());
}
+ @Test
+ void handleTooManyRequestsException_shouldReturnCorrectHttpStatus(){
+ var ex = new TooManyRequestsException("Too many requests");
+ ResponseEntity<RestError> response = underTest.handleTooManyRequestsException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.TOO_MANY_REQUESTS);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ex.getMessage());
+ }
+
@ParameterizedTest
@MethodSource("serverExceptionsProvider")
void handleServerException_shouldReturnCorrectHttpStatus(ServerException ex, HttpStatus expectedStatus) {
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java
index 9563ddd06fb..82ecaf12297 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java
@@ -26,6 +26,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.api.internal.MetadataLoader;
import org.sonar.api.utils.System2;
import org.sonar.api.utils.Version;
+import org.springframework.validation.Validator;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.util.UrlPathHelper;
@@ -65,4 +66,12 @@ public class CommonWebConfigTest {
}
}
+ @Test
+ public void getValidator_shouldReturnNonNull() {
+ CommonWebConfig commonWebConfig = new CommonWebConfig();
+ Validator validator = commonWebConfig.getValidator();
+
+ assertThat(validator).isNotNull();
+ }
+
}
diff --git a/server/sonar-webserver-webapi-v2/src/test/resources/http/http-client.env.json b/server/sonar-webserver-webapi-v2/src/test/resources/http/http-client.env.json
new file mode 100644
index 00000000000..c78a169cfdc
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/test/resources/http/http-client.env.json
@@ -0,0 +1,45 @@
+{
+ "localhost": {
+ "baseUrl": "http://localhost:9000",
+ "sonar": {
+ "token": "TODO: Replace with your SonarQube token in a private configuration file",
+ "tokenNoPermissions": "TODO: Replace with your SonarQube token in a private configuration file"
+ },
+ "github": {
+ "token": "TODO: Replace with your GitHub token in a private configuration file",
+ "repo": {
+ "slug": "jc-infinite-repos/jc-infinite-repos-3600",
+ "url": "https://github.com/jc-infinite-repos/jc-infinite-repos-3600",
+ "httpsUrl": "https://github.com/jc-infinite-repos/jc-infinite-repos-3600.git",
+ "sshUrl": "git@github.com:jc-infinite-repos/jc-infinite-repos-3600.git"
+ }
+ },
+ "gitlab": {
+ "token": "TODO: Replace with your GitLab token in a private configuration file",
+ "repo": {
+ "slug": "julien.camus1/ror-boilerplate",
+ "url": "https://gitlab.com/julien.camus1/ror-boilerplate",
+ "httpsUrl": "https://gitlab.com/julien.camus1/ror-boilerplate.git",
+ "sshUrl": "git@gitlab.com:julien.camus1/ror-boilerplate.git"
+ }
+ },
+ "azure": {
+ "token": "TODO: Replace with your Azure token in a private configuration file",
+ "repo": {
+ "slug": null,
+ "url": "https://dev.azure.com/juliencamus/test/_git/test1",
+ "httpsUrl": "https://juliencamus@dev.azure.com/juliencamus/test/_git/test1",
+ "sshUrl": "git@ssh.dev.azure.com:v3/juliencamus/test/test1"
+ }
+ },
+ "bitbucket": {
+ "token": "TODO: Replace with your Bitbucket token in a private configuration file",
+ "repo": {
+ "slug": "sonar-pk-test/sonarlint-onsite-exercise",
+ "url": "https://bitbucket.org/sonar-pk-test/sonarlint-onsite-exercise",
+ "httpsUrl": "https://juliencamus@bitbucket.org/sonar-pk-test/sonarlint-onsite-exercise.git",
+ "sshUrl": "git@bitbucket.org:sonar-pk-test/sonarlint-onsite-exercise.git"
+ }
+ }
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/test/resources/http/project-bindings.http b/server/sonar-webserver-webapi-v2/src/test/resources/http/project-bindings.http
new file mode 100644
index 00000000000..36247d92551
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/test/resources/http/project-bindings.http
@@ -0,0 +1,89 @@
+### Search project bindings by repository URL (HTTPS URL)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Search project bindings by repository URL (HTTPS URL with .git suffix)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.httpsUrl}}
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Search project bindings by repository URL (SSH URL)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.sshUrl}}
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Search project bindings by repository URL (URL with trailing slash)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}/
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Search project bindings by repository URL with pagination
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+ &pageIndex=1
+ &pageSize=10
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Error case: No ALM configuration found for repository URL
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl=https://github.com/nonexistent/repo
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Error case: Invalid URL format
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl=invalid-url-format
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Parameter validation error: repositoryUrl with repository parameter
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+ &repository={{github.repo.slug}}
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Parameter validation error: repositoryUrl with dopSettingId parameter
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+ &dopSettingId=25e6ceba-ab08-4b43-8655-52422dde80b6
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Parameter validation error: repositoryUrl with both repository and dopSettingId
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+ &repository={{github.repo.slug}}
+ &dopSettingId=25e6ceba-ab08-4b43-8655-52422dde80b6
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Traditional search by repository and dopSettingId (still supported)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repository={{github.repo.slug}}
+ &dopSettingId=25e6ceba-ab08-4b43-8655-52422dde80b6
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Traditional search by repository only
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repository={{github.repo.slug}}
+Authorization: Bearer {{sonar.token}}
+Accept: application/json
+
+### Error case: Missing authentication
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+Accept: application/json
+
+### Error case: Insufficient permissions (use token without PROVISION_PROJECTS permission)
+GET {{baseUrl}}/api/v2/dop-translation/project-bindings
+ ?repositoryUrl={{github.repo.url}}
+Authorization: Bearer {{sonar.tokenNoPermissions}}
+Accept: application/json
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java
index c89ca62f3c1..442b8272a26 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java
@@ -1863,6 +1863,41 @@ class SearchActionIT {
@ParameterizedTest
@ValueSource(booleans = {true, false})
+ void only_vulnerabilities_are_returned_by_owasp_mobile_2024(boolean mqrMode) {
+ doReturn(Optional.of(mqrMode)).when(config).getBoolean(MULTI_QUALITY_MODE_ENABLED);
+ ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
+ ComponentDto file = db.components().insertComponent(newFileDto(project));
+ Consumer<RuleDto> ruleConsumer = ruleDefinitionDto -> ruleDefinitionDto
+ .setSecurityStandards(Sets.newHashSet("cwe:20", "cwe:564", "cwe:89", "cwe:943", "owaspMobileTop10-2024:m3", "owaspTop10-2021:a2"))
+ .setSystemTags(Sets.newHashSet("bad-practice", "cwe", "owasp-a1", "sans-top25-insecure", "sql"));
+ Consumer<IssueDto> issueConsumer = issueDto -> issueDto.setTags(Sets.newHashSet("bad-practice", "cwe", "owasp-a1", "sans-top25" +
+ "-insecure", "sql"));
+ RuleDto hotspotRule = db.rules().insertHotspotRule(ruleConsumer);
+ db.issues().insertHotspot(hotspotRule, project, file, issueConsumer);
+ RuleDto issueRule = db.rules().insertIssueRule(ruleConsumer);
+ IssueDto issueDto1 = db.issues().insertIssue(issueRule, project, file, issueConsumer,
+ issueDto -> issueDto.setType(RuleType.VULNERABILITY).replaceAllImpacts(List.of(new ImpactDto(SECURITY, HIGH))));
+ IssueDto issueDto2 = db.issues().insertIssue(issueRule, project, file, issueConsumer,
+ issueDto -> issueDto.setType(RuleType.VULNERABILITY).replaceAllImpacts(List.of(new ImpactDto(SECURITY, HIGH))));
+ db.issues().insertIssue(issueRule, project, file, issueConsumer, issueDto -> issueDto.setType(CODE_SMELL));
+ indexPermissionsAndIssues();
+
+ SearchWsResponse result = ws.newRequest()
+ .setParam("owaspMobileTop10-2024", "m3")
+ .setParam(FACETS, "owaspMobileTop10-2024")
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(result.getIssuesList())
+ .extracting(Issue::getKey)
+ .containsExactlyInAnyOrder(issueDto1.getKey(), issueDto2.getKey());
+
+ assertThat(result.getFacets().getFacets(0).getValuesList())
+ .extracting(Common.FacetValue::getVal, Common.FacetValue::getCount)
+ .containsExactlyInAnyOrder(tuple("m3", 2L));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
void only_vulnerabilities_are_returned_by_stig_R5V3(boolean mqrMode) {
doReturn(Optional.of(mqrMode)).when(config).getBoolean(MULTI_QUALITY_MODE_ENABLED);
ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
@@ -2292,7 +2327,7 @@ class SearchActionIT {
"createdBefore", "createdInLast", "directories", "facets", "files", "issues", "scopes", "languages", "onComponentOnly",
"p", "projects", "ps", "resolutions", "resolved", "rules", "s", "severities", "statuses", "tags", "types", "pciDss-3.2", "pciDss-4" +
".0",
- "owaspAsvs-4.0",
+ "owaspAsvs-4.0", "owaspMobileTop10-2024",
"owaspAsvsLevel", "owaspTop10", "owaspTop10-2021", "stig-ASD_V5R3", "casa", "sansTop25", "cwe", "sonarsourceSecurity", "timeZone",
"inNewCodePeriod", "codeVariants",
"cleanCodeAttributeCategories", "impactSeverities", "impactSoftwareQualities", "issueStatuses", "fixedInPullRequest",
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java
index a6c7a15b94b..7f037d97ff8 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java
@@ -20,6 +20,7 @@
package org.sonar.server.issue.ws.anticipatedtransition;
import java.io.IOException;
+import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.Rule;
@@ -104,7 +105,7 @@ public class AnticipatedTransitionsActionIT {
}
@Test
- public void givenRequestWithTransitions_whenHandle_thenAllTransitionsAreSaved() throws IOException {
+ public void givenRequestWithTransitions_whenHandle_thenAllTransitionsAreSaved() throws IOException, URISyntaxException {
// given
ProjectDto projectDto = mockProjectDto();
mockUser(projectDto, ISSUE_ADMIN);
@@ -149,7 +150,7 @@ public class AnticipatedTransitionsActionIT {
}
@Test
- public void givenTransitionsForUserAndProjectAlreadyExistInDb_whenHandle_thenTheNewTransitionsShouldReplaceTheOldOnes() throws IOException {
+ public void givenTransitionsForUserAndProjectAlreadyExistInDb_whenHandle_thenTheNewTransitionsShouldReplaceTheOldOnes() throws IOException, URISyntaxException {
// given
ProjectDto projectDto = mockProjectDto();
mockUser(projectDto, ISSUE_ADMIN);
@@ -179,7 +180,7 @@ public class AnticipatedTransitionsActionIT {
}
@Test
- public void givenRequestWithNoTransitions_whenHandle_thenExistingTransitionsForUserAndProjectShouldBePurged() throws IOException {
+ public void givenRequestWithNoTransitions_whenHandle_thenExistingTransitionsForUserAndProjectShouldBePurged() throws IOException, URISyntaxException {
// given
ProjectDto projectDto = mockProjectDto();
mockUser(projectDto, ISSUE_ADMIN);
@@ -198,7 +199,7 @@ public class AnticipatedTransitionsActionIT {
}
@Test
- public void givenUserWithoutAdminIssuesPermission_whenHandle_thenThrowException() throws IOException {
+ public void givenUserWithoutAdminIssuesPermission_whenHandle_thenThrowException() throws IOException, URISyntaxException {
// given
ProjectDto projectDto = mockProjectDto();
mockUser(projectDto, CODEVIEWER);
@@ -234,8 +235,8 @@ public class AnticipatedTransitionsActionIT {
return projectDto;
}
- private String readTestResourceFile(String fileName) throws IOException {
- return Files.readString(Path.of(getClass().getResource(fileName).getPath()));
+ private String readTestResourceFile(String fileName) throws IOException, URISyntaxException {
+ return Files.readString(Path.of(getClass().getResource(fileName).toURI()));
}
}
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflowTest.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflowTest.java
new file mode 100644
index 00000000000..f261155a9db
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflowTest.java
@@ -0,0 +1,169 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.measure.live;
+
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+import org.sonar.server.qualitygate.QualityGate;
+import org.sonar.server.setting.ProjectConfigurationLoader;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
+
+@ExtendWith(MockitoExtension.class)
+class LiveMeasureUpdaterWorkflowTest {
+
+ private static final String TEST_KEY = "test_key";
+ @RegisterExtension
+ private final DbTester db = DbTester.create();
+ @Mock
+ private ProjectConfigurationLoader projectConfigurationLoader;
+ @Mock
+ private LiveQualityGateComputer liveQualityGateComputer;
+ @Mock
+ private Configuration config;
+ @Mock
+ private QualityGate qualityGate;
+ private DbClient dbClient;
+ private DbSession dbSession;
+ private ComponentDto project;
+ private BranchDto branch;
+
+ @BeforeEach
+ void setUp() {
+ this.dbClient = db.getDbClient();
+ this.dbSession = db.getSession();
+
+ this.project = db.components().insertPublicProject().getMainBranchComponent();
+ this.branch = dbClient.branchDao().selectByUuid(dbSession, project.uuid()).get();
+
+ var projectDto = dbClient.projectDao().selectByUuid(dbSession, branch.getProjectUuid()).get();
+
+ lenient().when(projectConfigurationLoader.loadBranchConfiguration(dbSession, branch)).thenReturn(config);
+ lenient().when(liveQualityGateComputer.loadQualityGate(dbSession, projectDto, branch)).thenReturn(qualityGate);
+ }
+
+ private LiveMeasureUpdaterWorkflow underTest() {
+ return LiveMeasureUpdaterWorkflow.build(
+ dbClient,
+ dbSession,
+ project,
+ projectConfigurationLoader,
+ liveQualityGateComputer);
+ }
+
+ @Test
+ void build_whenNoBranch_thenFails() {
+ var component = new ComponentDto().setUuid("whatever");
+
+ assertThatThrownBy(() -> LiveMeasureUpdaterWorkflow.build(
+ dbClient,
+ dbSession,
+ component,
+ projectConfigurationLoader,
+ liveQualityGateComputer)).isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void build_whenBranchAndProject_thenLoadsAllData() {
+ var result = underTest();
+
+ assertThat(result.getBranchDto()).isEqualTo(this.branch);
+ assertThat(result.getConfig()).isEqualTo(config);
+ }
+
+ @Test
+ void updateQualityGateMeasures() {
+ var metric = db.measures().insertMetric(m -> m.setKey(TEST_KEY));
+ var measure = db.measures().insertMeasure(project, m -> m.getMetricValues().put(TEST_KEY, 1));
+ var expectedResult = mock(EvaluatedQualityGate.class);
+
+ when(liveQualityGateComputer.refreshGateStatus(eq(project), eq(qualityGate), any(MeasureMatrix.class), eq(config))).thenReturn(expectedResult);
+
+ var measureMatrix = new MeasureMatrix(
+ List.of(project),
+ List.of(metric),
+ List.of(measure));
+
+ measureMatrix.setValue(project, TEST_KEY, 2);
+
+ assertThat(underTest().updateQualityGateMeasures(measureMatrix)).isEqualTo(expectedResult);
+
+ var updatedMeasures = dbClient.measureDao().selectByComponentUuid(dbSession, project.uuid());
+
+ assertThat(updatedMeasures.get().getLong(TEST_KEY)).isEqualTo(2);
+ }
+
+ @Test
+ void getKeysOfAllInvolvedMetrics() {
+ var metric = mock(Metric.class);
+ when(metric.getKey()).thenReturn(TEST_KEY);
+
+ var formulaFactory = mock(MeasureUpdateFormulaFactory.class);
+ when(formulaFactory.getFormulaMetrics()).thenReturn(Set.of(metric));
+ when(liveQualityGateComputer.getMetricsRelatedTo(qualityGate)).thenReturn(Set.of("other"));
+
+ var result = underTest().getKeysOfAllInvolvedMetrics(formulaFactory);
+
+ assertThat(result).containsExactlyInAnyOrder("other", TEST_KEY);
+ }
+
+ @Test
+ void buildMeasureMatrix() {
+ db.measures().insertMetric(m -> m.setKey(TEST_KEY));
+ db.measures().insertMeasure(project, m -> m.getMetricValues().put(TEST_KEY, 1.0));
+
+ var result = underTest().buildMeasureMatrix(
+ List.of(TEST_KEY),
+ Set.of(branch.getUuid()));
+
+ assertThat(result.getMeasure(project, TEST_KEY).get().getValue()).isEqualTo(1.0);
+ }
+
+ @Test
+ void loadPreviousStatus() {
+ db.measures().insertMetric(m -> m.setKey(ALERT_STATUS_KEY));
+ db.measures().insertMeasure(project, m -> m.getMetricValues().put(ALERT_STATUS_KEY, Metric.Level.OK));
+
+ var result = underTest().loadPreviousStatus();
+
+ assertThat(result).isEqualTo(Metric.Level.OK);
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateCaycCheckerIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateCaycCheckerIT.java
index 343e1854127..7b660e38620 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateCaycCheckerIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/QualityGateCaycCheckerIT.java
@@ -39,7 +39,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.sonar.api.measures.CoreMetrics.BLOCKER_VIOLATIONS;
import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES;
-import static org.sonar.api.measures.CoreMetrics.FUNCTION_COMPLEXITY;
import static org.sonar.api.measures.CoreMetrics.LINE_COVERAGE;
import static org.sonar.api.measures.CoreMetrics.NEW_COVERAGE;
import static org.sonar.api.measures.CoreMetrics.NEW_DUPLICATED_LINES_DENSITY;
@@ -107,7 +106,7 @@ public class QualityGateCaycCheckerIT {
@Test
public void isCaycCondition_when_check_non_compliant_condition_should_return_false() {
- List.of(BLOCKER_VIOLATIONS, FUNCTION_COMPLEXITY)
+ List.of(BLOCKER_VIOLATIONS)
.stream().map(this::toMetricDto)
.forEach(metricDto -> assertFalse(underTest.isCaycCondition(metricDto)));
}
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java
index cd482f865e3..78c9141725f 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java
@@ -20,18 +20,16 @@
package org.sonar.server.setting.ws;
import com.google.gson.Gson;
-import com.tngtech.java.junit.dataprovider.DataProvider;
-import com.tngtech.java.junit.dataprovider.DataProviderRunner;
-import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.net.HttpURLConnection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import javax.annotation.Nullable;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
import org.sonar.api.PropertyType;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.config.PropertyDefinition.ConfigScope;
@@ -40,7 +38,6 @@ import org.sonar.api.config.PropertyFieldDefinition;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.Param;
import org.sonar.api.utils.System2;
-import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
@@ -48,6 +45,7 @@ import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentQualifiers;
import org.sonar.db.component.ComponentTesting;
import org.sonar.db.component.ProjectData;
+import org.sonar.db.permission.ProjectPermission;
import org.sonar.db.portfolio.PortfolioDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.property.PropertyDbTester;
@@ -78,15 +76,89 @@ import static org.sonar.db.property.PropertyTesting.newComponentPropertyDto;
import static org.sonar.db.property.PropertyTesting.newGlobalPropertyDto;
import static org.sonar.db.user.UserTesting.newUserDto;
-@RunWith(DataProviderRunner.class)
-public class SetActionIT {
+class SetActionIT {
private static final Gson GSON = GsonHelper.create();
- @Rule
- public UserSessionRule userSession = UserSessionRule.standalone().logIn();
- @Rule
- public DbTester db = DbTester.create(System2.INSTANCE);
+ private static final String SECURITY_CUSTOM_CONFIG_INCORRECT_TYPE = """
+ {
+ "S3649": {
+ "sources": [
+ {
+ "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
+ }
+ ],
+ "sinks": [
+ {
+ "methodId": 12345,
+ "args": [1]
+ }
+ ]
+ }
+ }""";
+
+ private static final String SECURITY_CUSTOM_CONFIG_NO_ARGS = """
+ {
+ "S3649": {
+ "sources": [
+ {
+ "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
+ }
+ ],
+ "sanitizers": [
+ {
+ "methodId": "SomeSanitizer"
+ }
+ ]
+ }
+ }""";
+
+ private static final String SECURITY_CUSTOM_CONFIG_EMPTY_ARGS_ARRAY = """
+ {
+ "S3649": {
+ "sources": [
+ {
+ "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
+ }
+ ],
+ "validators": [
+ {
+ "methodId": "SomeValidator",
+ "args": []
+ }
+ ]
+ }
+ }""";
+
+ private static final String SECURITY_CUSTOM_CONFIG_UNKNOWN_ATTRIBUTE = """
+ {
+ "S3649": {
+ "sources": [
+ {
+ "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
+ }
+ ],
+ "unknown": [
+ {
+ "methodId": 12345,
+ "args": [1]
+ }
+ ]
+ }
+ }""";
+ private static final String SONAR_SECURITY_CONFIG_JAVA_SECURITY = "sonar.security.config.javasecurity";
+ private static final String SONAR_SECURITY_CONFIG_PHP_SECURITY = "sonar.security.config.phpsecurity";
+ private static final String SONAR_SECURITY_CONFIG_PYTHON_SECURITY = "sonar.security.config.pythonsecurity";
+ private static final String SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS = "sonar.security.config.roslyn.sonaranalyzer.security.cs";
+ private static final String ERROR_INCORRECT_TYPE = "Provided JSON is invalid : [expected type: string, actual: integer at mem://input#/S3649/sinks/0/methodId (Line 10, character 21)]";
+ private static final String ERROR_NO_ARGS = "Provided JSON is invalid : [required properties are missing: args at mem://input#/S3649/sanitizers/0 (Line 9, character 8)]";
+ private static final String ERROR_EMPTY_ARGS_ARRAY = "Provided JSON is invalid : [expected minimum items: 1, found only 0 at mem://input#/S3649/validators/0/args (Line 11, character 18)]";
+ private static final String ERROR_UNKNOWN_ATTRIBUTE = "Provided JSON is invalid : [false schema always fails at mem://input#/S3649/unknown (Line 8, character 16)]";
+
+ @RegisterExtension
+ private final UserSessionRule userSession = UserSessionRule.standalone().logIn();
+ @RegisterExtension
+ private final DbTester db = DbTester.create(System2.INSTANCE);
private final PropertyDbTester propertyDb = new PropertyDbTester(db);
private final DbClient dbClient = db.getDbClient();
@@ -100,24 +172,47 @@ public class SetActionIT {
private final WsActionTester ws = new WsActionTester(underTest);
- @Before
- public void setUp() {
+ @BeforeEach
+ void setUp() {
// by default test doesn't care about permissions
userSession.logIn().setSystemAdministrator();
}
+
+ static Object[][] securityJsonProperties() {
+ return new Object[][] {
+ {SONAR_SECURITY_CONFIG_JAVA_SECURITY},
+ {SONAR_SECURITY_CONFIG_PHP_SECURITY},
+ {SONAR_SECURITY_CONFIG_PYTHON_SECURITY},
+ {SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS}
+ };
+ }
- @DataProvider
- public static Object[][] securityJsonProperties() {
+ static Object[][] securityJsonPropertiesForInvalidJsonTest() {
return new Object[][] {
- {"sonar.security.config.javasecurity"},
- {"sonar.security.config.phpsecurity"},
- {"sonar.security.config.pythonsecurity"},
- {"sonar.security.config.roslyn.sonaranalyzer.security.cs"}
+ {SONAR_SECURITY_CONFIG_JAVA_SECURITY, SECURITY_CUSTOM_CONFIG_INCORRECT_TYPE, ERROR_INCORRECT_TYPE},
+ {SONAR_SECURITY_CONFIG_JAVA_SECURITY, SECURITY_CUSTOM_CONFIG_NO_ARGS, ERROR_NO_ARGS},
+ {SONAR_SECURITY_CONFIG_JAVA_SECURITY, SECURITY_CUSTOM_CONFIG_EMPTY_ARGS_ARRAY, ERROR_EMPTY_ARGS_ARRAY},
+ {SONAR_SECURITY_CONFIG_JAVA_SECURITY, SECURITY_CUSTOM_CONFIG_UNKNOWN_ATTRIBUTE, ERROR_UNKNOWN_ATTRIBUTE},
+
+ {SONAR_SECURITY_CONFIG_PHP_SECURITY, SECURITY_CUSTOM_CONFIG_INCORRECT_TYPE, ERROR_INCORRECT_TYPE},
+ {SONAR_SECURITY_CONFIG_PHP_SECURITY, SECURITY_CUSTOM_CONFIG_NO_ARGS, ERROR_NO_ARGS},
+ {SONAR_SECURITY_CONFIG_PHP_SECURITY, SECURITY_CUSTOM_CONFIG_EMPTY_ARGS_ARRAY, ERROR_EMPTY_ARGS_ARRAY},
+ {SONAR_SECURITY_CONFIG_PHP_SECURITY, SECURITY_CUSTOM_CONFIG_UNKNOWN_ATTRIBUTE, ERROR_UNKNOWN_ATTRIBUTE},
+
+ {SONAR_SECURITY_CONFIG_PYTHON_SECURITY, SECURITY_CUSTOM_CONFIG_INCORRECT_TYPE, ERROR_INCORRECT_TYPE},
+ {SONAR_SECURITY_CONFIG_PYTHON_SECURITY, SECURITY_CUSTOM_CONFIG_NO_ARGS, ERROR_NO_ARGS},
+ {SONAR_SECURITY_CONFIG_PYTHON_SECURITY, SECURITY_CUSTOM_CONFIG_EMPTY_ARGS_ARRAY, ERROR_EMPTY_ARGS_ARRAY},
+ {SONAR_SECURITY_CONFIG_PYTHON_SECURITY, SECURITY_CUSTOM_CONFIG_UNKNOWN_ATTRIBUTE, ERROR_UNKNOWN_ATTRIBUTE},
+
+ {SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS, SECURITY_CUSTOM_CONFIG_INCORRECT_TYPE, ERROR_INCORRECT_TYPE},
+ {SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS, SECURITY_CUSTOM_CONFIG_NO_ARGS, ERROR_NO_ARGS},
+ {SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS, SECURITY_CUSTOM_CONFIG_EMPTY_ARGS_ARRAY, ERROR_EMPTY_ARGS_ARRAY},
+ {SONAR_SECURITY_CONFIG_ROSLYN_SECURITY_CS, SECURITY_CUSTOM_CONFIG_UNKNOWN_ATTRIBUTE, ERROR_UNKNOWN_ATTRIBUTE}
};
}
@Test
- public void empty_204_response() {
+ void empty_204_response() {
TestResponse result = ws.newRequest()
.setParam("key", "my.key")
.setParam("value", "my value")
@@ -128,7 +223,7 @@ public class SetActionIT {
}
@Test
- public void persist_new_global_setting() {
+ void persist_new_global_setting() {
callForGlobalSetting("my.key", "my,value");
assertGlobalSetting("my.key", "my,value");
@@ -136,7 +231,7 @@ public class SetActionIT {
}
@Test
- public void update_existing_global_setting() {
+ void update_existing_global_setting() {
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my value"), null, null, null, null);
assertGlobalSetting("my.key", "my value");
@@ -147,7 +242,7 @@ public class SetActionIT {
}
@Test
- public void persist_new_project_setting() {
+ void persist_new_project_setting() {
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my global value"), null, null, null, null);
ProjectDto project = db.components().insertPrivateProject().getProjectDto();
logInAsProjectAdministrator(project);
@@ -160,7 +255,7 @@ public class SetActionIT {
}
@Test
- public void persist_new_subportfolio_setting() {
+ void persist_new_subportfolio_setting() {
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my global value"), null, null, null, null);
ComponentDto portfolio = db.components().insertPrivatePortfolio();
ComponentDto subportfolio = db.components().insertSubportfolio(portfolio);
@@ -174,7 +269,7 @@ public class SetActionIT {
}
@Test
- public void persist_project_property_with_project_admin_permission() {
+ void persist_project_property_with_project_admin_permission() {
ProjectDto project = db.components().insertPrivateProject().getProjectDto();
logInAsProjectAdministrator(project);
@@ -184,7 +279,7 @@ public class SetActionIT {
}
@Test
- public void update_existing_project_setting() {
+ void update_existing_project_setting() {
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my global value"), null, null,
null, null);
ProjectDto project = db.components().insertPrivateProject().getProjectDto();
@@ -199,7 +294,7 @@ public class SetActionIT {
}
@Test
- public void persist_several_multi_value_setting() {
+ void persist_several_multi_value_setting() {
callForMultiValueGlobalSetting("my.key", List.of("first,Value", "second,Value", "third,Value"));
String expectedValue = "first%2CValue,second%2CValue,third%2CValue";
@@ -208,14 +303,14 @@ public class SetActionIT {
}
@Test
- public void persist_one_multi_value_setting() {
+ void persist_one_multi_value_setting() {
callForMultiValueGlobalSetting("my.key", List.of("first,Value"));
assertGlobalSetting("my.key", "first%2CValue");
}
@Test
- public void persist_property_set_setting() {
+ void persist_property_set_setting() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -252,7 +347,7 @@ public class SetActionIT {
}
@Test
- public void update_property_set_setting() {
+ void update_property_set_setting() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -299,7 +394,7 @@ public class SetActionIT {
}
@Test
- public void update_property_set_on_component_setting() {
+ void update_property_set_on_component_setting() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -350,7 +445,7 @@ public class SetActionIT {
}
@Test
- public void persist_multi_value_with_type_logIn() {
+ void persist_multi_value_with_type_logIn() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -370,7 +465,7 @@ public class SetActionIT {
}
@Test
- public void user_setting_is_not_updated() {
+ void user_setting_is_not_updated() {
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my user value").setUserUuid("42"), null, null,
null, "user_login");
propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my global value"), null, null, null, null);
@@ -382,7 +477,7 @@ public class SetActionIT {
}
@Test
- public void persist_global_property_with_deprecated_key() {
+ void persist_global_property_with_deprecated_key() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.deprecatedKey("my.old.key")
@@ -400,7 +495,7 @@ public class SetActionIT {
}
@Test
- public void persist_JSON_property() {
+ void persist_JSON_property() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -416,7 +511,7 @@ public class SetActionIT {
}
@Test
- public void fail_if_JSON_invalid_for_JSON_property() {
+ void fail_if_JSON_invalid_for_JSON_property() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -439,10 +534,10 @@ public class SetActionIT {
.hasMessage("Provided JSON is invalid");
}
- @Test
- @UseDataProvider("securityJsonProperties")
- public void successfully_validate_json_schema(String securityPropertyKey) {
- String security_custom_config = """
+ @ParameterizedTest
+ @MethodSource("securityJsonProperties")
+ void successfully_validate_json_schema(String securityPropertyKey) {
+ String securityCustomConfig = """
{
"S3649": {
"sources": [
@@ -481,128 +576,14 @@ public class SetActionIT {
.type(PropertyType.JSON)
.build());
- callForGlobalSetting(securityPropertyKey, security_custom_config);
-
- assertGlobalSetting(securityPropertyKey, security_custom_config);
- }
-
- @Test
- @UseDataProvider("securityJsonProperties")
- public void fail_json_schema_validation_when_property_has_incorrect_type(String securityPropertyKey) {
- String security_custom_config = """
- {
- "S3649": {
- "sources": [
- {
- "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
- }
- ],
- "sinks": [
- {
- "methodId": 12345,
- "args": [1]
- }
- ]
- }
- }""";
- definitions.addComponent(PropertyDefinition
- .builder(securityPropertyKey)
- .name("foo")
- .description("desc")
- .category("cat")
- .subCategory("subCat")
- .type(PropertyType.JSON)
- .build());
-
- assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, security_custom_config))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("expected type: string, actual: integer at line 10, character 21, pointer: #/S3649/sinks/0/methodId");
- }
-
- @Test
- @UseDataProvider("securityJsonProperties")
- public void fail_json_schema_validation_when_sanitizers_have_no_args(String securityPropertyKey) {
- String security_custom_config = """
- {
- "S3649": {
- "sources": [
- {
- "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
- }
- ],
- "sanitizers": [
- {
- "methodId": "SomeSanitizer"
- }
- ]
- }
- }""";
- definitions.addComponent(PropertyDefinition
- .builder(securityPropertyKey)
- .name("foo")
- .description("desc")
- .category("cat")
- .subCategory("subCat")
- .type(PropertyType.JSON)
- .build());
-
- assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, security_custom_config))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("required properties are missing: args at line 9, character 8, pointer: #/S3649/sanitizers/0");
- }
-
- @Test
- @UseDataProvider("securityJsonProperties")
- public void fail_json_schema_validation_when_validators_have_empty_args_array(String securityPropertyKey) {
- String security_custom_config = """
- {
- "S3649": {
- "sources": [
- {
- "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
- }
- ],
- "validators": [
- {
- "methodId": "SomeValidator",
- "args": []
- }
- ]
- }
- }""";
- definitions.addComponent(PropertyDefinition
- .builder(securityPropertyKey)
- .name("foo")
- .description("desc")
- .category("cat")
- .subCategory("subCat")
- .type(PropertyType.JSON)
- .build());
+ callForGlobalSetting(securityPropertyKey, securityCustomConfig);
- assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, security_custom_config))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("expected minimum items: 1, found only 0 at line 11, character 18, pointer: #/S3649/validators/0/args");
+ assertGlobalSetting(securityPropertyKey, securityCustomConfig);
}
- @Test
- @UseDataProvider("securityJsonProperties")
- public void fail_json_schema_validation_when_property_has_unknown_attribute(String securityPropertyKey) {
- String security_custom_config = """
- {
- "S3649": {
- "sources": [
- {
- "methodId": "My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery"
- }
- ],
- "unknown": [
- {
- "methodId": 12345,
- "args": [1]
- }
- ]
- }
- }""";
+ @ParameterizedTest
+ @MethodSource("securityJsonPropertiesForInvalidJsonTest")
+ void fail_json_schema_validation_when_property_has_incorrect_type(String securityPropertyKey, String customConfig, String expectedErrorMessage) {
definitions.addComponent(PropertyDefinition
.builder(securityPropertyKey)
.name("foo")
@@ -612,13 +593,13 @@ public class SetActionIT {
.type(PropertyType.JSON)
.build());
- assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, security_custom_config))
+ assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, customConfig))
.isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("false schema always fails at line 8, character 16, pointer: #/S3649/unknown");
+ .hasMessageContaining(expectedErrorMessage);
}
@Test
- public void persist_global_setting_with_non_ascii_characters() {
+ void persist_global_setting_with_non_ascii_characters() {
callForGlobalSetting("my.key", "fi±∞…");
assertGlobalSetting("my.key", "fi±∞…");
@@ -626,34 +607,34 @@ public class SetActionIT {
}
@Test
- public void fail_when_no_key() {
+ void fail_when_no_key() {
assertThatThrownBy(() -> callForGlobalSetting(null, "my value"))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
- public void fail_when_empty_key_value() {
+ void fail_when_empty_key_value() {
assertThatThrownBy(() -> callForGlobalSetting(" ", "my value"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("The 'key' parameter is missing");
}
@Test
- public void fail_when_no_value() {
+ void fail_when_no_value() {
assertThatThrownBy(() -> callForGlobalSetting("my.key", null))
.isInstanceOf(BadRequestException.class)
.hasMessage("Either 'value', 'values' or 'fieldValues' must be provided");
}
@Test
- public void fail_when_empty_value() {
+ void fail_when_empty_value() {
assertThatThrownBy(() -> callForGlobalSetting("my.key", ""))
.isInstanceOf(BadRequestException.class)
.hasMessage("A non empty value must be provided");
}
@Test
- public void fail_when_one_empty_value_on_multi_value() {
+ void fail_when_one_empty_value_on_multi_value() {
List<String> values = List.of("oneValue", " ", "anotherValue");
assertThatThrownBy(() -> callForMultiValueGlobalSetting("my.key", values))
.isInstanceOf(BadRequestException.class)
@@ -661,7 +642,7 @@ public class SetActionIT {
}
@Test
- public void throw_ForbiddenException_if_not_system_administrator() {
+ void throw_ForbiddenException_if_not_system_administrator() {
userSession.logIn().setNonSystemAdministrator();
assertThatThrownBy(() -> callForGlobalSetting("my.key", "my value"))
@@ -670,7 +651,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_data_and_type_do_not_match() {
+ void fail_when_data_and_type_do_not_match() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -688,7 +669,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_data_and_login_type_with_invalid_logIn() {
+ void fail_when_data_and_login_type_with_invalid_logIn() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -709,7 +690,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_data_and_type_do_not_match_with_unknown_error_key() {
+ void fail_when_data_and_type_do_not_match_with_unknown_error_key() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -727,7 +708,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_global_with_property_only_on_projects() {
+ void fail_when_global_with_property_only_on_projects() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -745,7 +726,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_view_property_when_on_projects_only() {
+ void fail_when_view_property_when_on_projects_only() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -768,7 +749,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_with_definition_when_component_qualifier_does_not_match() {
+ void fail_when_property_with_definition_when_component_qualifier_does_not_match() {
PortfolioDto portfolio = db.components().insertPrivatePortfolioDto();
definitions.addComponent(PropertyDefinition
.builder("my.key")
@@ -789,40 +770,40 @@ public class SetActionIT {
}
@Test
- public void succeed_for_property_without_definition_when_set_on_project_component() {
+ void succeed_for_property_without_definition_when_set_on_project_component() {
ProjectDto project = randomPublicOrPrivateProject().getProjectDto();
succeedForPropertyWithoutDefinitionAndValidComponent(project);
}
@Test
- public void fail_for_property_without_definition_when_set_on_directory_component() {
+ void fail_for_property_without_definition_when_set_on_directory_component() {
ProjectData projectData = randomPublicOrPrivateProject();
ComponentDto directory = db.components().insertComponent(ComponentTesting.newDirectory(projectData.getMainBranchComponent(), "A/B"));
failForPropertyWithoutDefinitionOnUnsupportedComponent(projectData.getProjectDto(), directory);
}
@Test
- public void fail_for_property_without_definition_when_set_on_file_component() {
+ void fail_for_property_without_definition_when_set_on_file_component() {
ProjectData projectData = randomPublicOrPrivateProject();
ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(projectData.getMainBranchComponent()));
failForPropertyWithoutDefinitionOnUnsupportedComponent(projectData.getProjectDto(), file);
}
@Test
- public void succeed_for_property_without_definition_when_set_on_view_component() {
+ void succeed_for_property_without_definition_when_set_on_view_component() {
PortfolioDto view = db.components().insertPrivatePortfolioDto();
succeedForPropertyWithoutDefinitionAndValidComponent(view);
}
@Test
- public void succeed_for_property_without_definition_when_set_on_subview_component() {
+ void succeed_for_property_without_definition_when_set_on_subview_component() {
ComponentDto view = db.components().insertPrivatePortfolio();
ComponentDto subview = db.components().insertComponent(ComponentTesting.newSubPortfolio(view));
failForPropertyWithoutDefinitionOnUnsupportedComponent(db.components().getPortfolioDto(view), subview);
}
@Test
- public void fail_for_property_without_definition_when_set_on_projectCopy_component() {
+ void fail_for_property_without_definition_when_set_on_projectCopy_component() {
ComponentDto view = db.components().insertPrivatePortfolio();
ComponentDto projectCopy = db.components().insertComponent(ComponentTesting.newProjectCopy("a", db.components().insertPrivateProject().getMainBranchComponent(), view));
@@ -864,7 +845,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_single_and_multi_value_provided() {
+ void fail_when_single_and_multi_value_provided() {
List<String> value = List.of("Another Value");
assertThatThrownBy(() -> call("my.key", "My Value", value, null, null))
.isInstanceOf(BadRequestException.class)
@@ -872,7 +853,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_multi_definition_and_single_value_provided() {
+ void fail_when_multi_definition_and_single_value_provided() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -888,7 +869,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_single_definition_and_multi_value_provided() {
+ void fail_when_single_definition_and_multi_value_provided() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -904,7 +885,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_empty_values_on_one_property_set() {
+ void fail_when_empty_values_on_one_property_set() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -933,7 +914,7 @@ public class SetActionIT {
}
@Test
- public void do_not_fail_when_only_one_empty_value_on_one_property_set() {
+ void do_not_fail_when_only_one_empty_value_on_one_property_set() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -962,14 +943,14 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_setting_is_not_defined() {
+ void fail_when_property_set_setting_is_not_defined() {
assertThatThrownBy(() -> callForGlobalPropertySet("my.key", singletonList("{\"field\":\"value\"}")))
.isInstanceOf(BadRequestException.class)
.hasMessage("Setting 'my.key' is undefined");
}
@Test
- public void fail_when_property_set_with_unknown_field() {
+ void fail_when_property_set_with_unknown_field() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -992,7 +973,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_has_field_with_incorrect_type() {
+ void fail_when_property_set_has_field_with_incorrect_type() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1015,7 +996,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_has_a_null_field_value() {
+ void fail_when_property_set_has_a_null_field_value() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1038,7 +1019,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_with_invalid_json() {
+ void fail_when_property_set_with_invalid_json() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1062,7 +1043,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_with_json_of_the_wrong_format() {
+ void fail_when_property_set_with_json_of_the_wrong_format() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1086,7 +1067,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_property_set_on_component_of_global_setting() {
+ void fail_when_property_set_on_component_of_global_setting() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1108,7 +1089,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_component_not_found() {
+ void fail_when_component_not_found() {
TestRequest testRequest = ws.newRequest()
.setParam("key", "foo")
.setParam("value", "2")
@@ -1119,7 +1100,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_setting_key_is_defined_in_sonar_properties() {
+ void fail_when_setting_key_is_defined_in_sonar_properties() {
ProjectDto project = db.components().insertPrivateProject().getProjectDto();
logInAsProjectAdministrator(project);
String settingKey = ProcessProperties.Property.JDBC_URL.getKey();
@@ -1133,8 +1114,7 @@ public class SetActionIT {
.hasMessage(format("Setting '%s' can only be used in sonar.properties", settingKey));
}
- @DataProvider
- public static Object[][] forbiddenProperties() {
+ static Object[][] forbiddenProperties() {
return new Object[][] {
{GITLAB_AUTH_URL},
{GITHUB_API_URL},
@@ -1143,9 +1123,9 @@ public class SetActionIT {
};
}
- @Test
- @UseDataProvider("forbiddenProperties")
- public void fail_when_setting_key_is_forbidden(String property) {
+ @ParameterizedTest
+ @MethodSource("forbiddenProperties")
+ void fail_when_setting_key_is_forbidden(String property) {
TestRequest testRequest = ws.newRequest()
.setParam("key", property)
.setParam("value", "value");
@@ -1155,7 +1135,7 @@ public class SetActionIT {
}
@Test
- public void fail_when_setting_key_is_forbidden() {
+ void fail_when_setting_key_is_forbidden() {
TestRequest testRequest = ws.newRequest()
.setParam("key", "sonar.auth.gitlab.url")
.setParam("value", "http://malicious.url");
@@ -1165,7 +1145,7 @@ public class SetActionIT {
}
@Test
- public void definition() {
+ void definition() {
WebService.Action definition = ws.getDef();
assertThat(definition.key()).isEqualTo("set");
@@ -1177,7 +1157,7 @@ public class SetActionIT {
}
@Test
- public void call_whenEmailPropertyValid_shouldSucceed() {
+ void call_whenEmailPropertyValid_shouldSucceed() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
@@ -1190,7 +1170,7 @@ public class SetActionIT {
}
@Test
- public void call_whenEmailPropertyInvalid_shouldFail() {
+ void call_whenEmailPropertyInvalid_shouldFail() {
definitions.addComponent(PropertyDefinition
.builder("my.key")
.name("foo")
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
index c81bb3c4908..d86d4653534 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
@@ -103,10 +103,8 @@ import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueW
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.RESOLVE;
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.UNCONFIRM;
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.WONT_FIX;
-import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.OPEN_AS_VULNERABILITY;
import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.RESET_AS_TO_REVIEW;
import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.RESOLVE_AS_REVIEWED;
-import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.SET_AS_IN_REVIEW;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_BULK_CHANGE;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ADD_TAGS;
@@ -162,7 +160,7 @@ public class BulkChangeAction implements IssuesWsAction {
new Change("10.4", "Transition '%s' is now supported.".formatted(ACCEPT)),
new Change("10.2", format("Parameters '%s' and '%s' are now deprecated.", PARAM_SET_SEVERITY, PARAM_SET_TYPE)),
new Change("8.2", "Security hotspots are no longer supported and will be ignored."),
- new Change("8.2", format("Transitions '%s', '%s' and '%s' are no more supported", SET_AS_IN_REVIEW, RESOLVE_AS_REVIEWED, OPEN_AS_VULNERABILITY)),
+ new Change("8.2", format("Transitions '%s', '%s' and '%s' are no more supported", "setinreview", RESOLVE_AS_REVIEWED, "openasvulnerability")),
new Change("6.3", "'actions' parameter is ignored"))
.setHandler(this)
.setResponseExample(getClass().getResource("bulk_change-example.json"))
@@ -194,7 +192,6 @@ public class BulkChangeAction implements IssuesWsAction {
RESOLVE.getKey(),
FALSE_POSITIVE.getKey(),
WONT_FIX.getKey(),
- SET_AS_IN_REVIEW.getKey(),
RESOLVE_AS_REVIEWED.getKey(),
RESET_AS_TO_REVIEW.getKey(),
ACCEPT.getKey()));
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
index db6e3d28029..12dfc14d9ec 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
@@ -51,10 +51,8 @@ import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueW
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.RESOLVE;
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.UNCONFIRM;
import static org.sonar.server.issue.workflow.codequalityissue.CodeQualityIssueWorkflowTransition.WONT_FIX;
-import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.OPEN_AS_VULNERABILITY;
import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.RESET_AS_TO_REVIEW;
import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.RESOLVE_AS_REVIEWED;
-import static org.sonar.server.issue.workflow.securityhotspot.SecurityHotspotWorkflowTransition.SET_AS_IN_REVIEW;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_DO_TRANSITION;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TRANSITION;
@@ -104,8 +102,8 @@ public class DoTransitionAction implements IssuesWsAction {
new Change("10.2", "Add 'impacts', 'cleanCodeAttribute', 'cleanCodeAttributeCategory' fields to the response"),
new Change("9.6", "Response field 'ruleDescriptionContextKey' added"),
new Change("8.8", "The response field components.uuid is removed"),
- new Change("8.1", format("transitions '%s' and '%s' are no more supported", SET_AS_IN_REVIEW, OPEN_AS_VULNERABILITY)),
- new Change("7.8", format("added '%s', %s, %s and %s transitions for security hotspots ", SET_AS_IN_REVIEW, RESOLVE_AS_REVIEWED, OPEN_AS_VULNERABILITY, RESET_AS_TO_REVIEW)),
+ new Change("8.1", format("transitions '%s' and '%s' are no more supported", "setinreview", "openasvulnerability")),
+ new Change("7.8", format("added '%s', %s, %s and %s transitions for security hotspots ", "setinreview", RESOLVE_AS_REVIEWED, "openasvulnerability", RESET_AS_TO_REVIEW)),
new Change("7.3", "added transitions for security hotspots"),
new Change("6.5", "the database ids of the components are removed from the response"),
new Change("6.5", "the response field components.uuid is deprecated. Use components.key instead."))
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java
index f16ce3bfaf5..5fe19dd0908 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java
@@ -37,7 +37,6 @@ import org.sonar.api.issue.IssueStatus;
import org.sonar.api.issue.impact.SoftwareQuality;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.CleanCodeAttributeCategory;
-import org.sonar.core.rule.RuleType;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
@@ -45,6 +44,7 @@ import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.Param;
import org.sonar.api.utils.Paging;
import org.sonar.api.utils.System2;
+import org.sonar.core.rule.RuleType;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.user.UserDto;
@@ -72,7 +72,6 @@ import static java.util.Optional.ofNullable;
import static org.sonar.api.issue.Issue.RESOLUTIONS;
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
import static org.sonar.api.issue.Issue.RESOLUTION_REMOVED;
-import static org.sonar.api.issue.Issue.STATUS_IN_REVIEW;
import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.api.issue.Issue.STATUS_REOPENED;
import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
@@ -126,6 +125,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ON_COMPONENT_ONLY;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_ASVS_40;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_ASVS_LEVEL;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_MOBILE_TOP_10_2024;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10_2021;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PCI_DSS_32;
@@ -169,6 +169,7 @@ public class SearchAction implements IssuesWsAction {
PARAM_PCI_DSS_32,
PARAM_PCI_DSS_40,
PARAM_OWASP_ASVS_40,
+ PARAM_OWASP_MOBILE_TOP_10_2024,
PARAM_OWASP_TOP_10,
PARAM_OWASP_TOP_10_2021,
PARAM_STIG_ASD_V5R3,
@@ -188,6 +189,7 @@ public class SearchAction implements IssuesWsAction {
"the componentKeys parameter. ";
private static final String NEW_FACET_ADDED_MESSAGE = "Facet '%s' has been added";
private static final String NEW_PARAM_ADDED_MESSAGE = "Param '%s' has been added";
+ private static final String V_2025_3 = "2025.3";
private static final Set<String> FACETS_REQUIRING_PROJECT = newHashSet(PARAM_FILES, PARAM_DIRECTORIES);
private final UserSession userSession;
@@ -222,6 +224,8 @@ public class SearchAction implements IssuesWsAction {
+ "<br/>When issue indexing is in progress returns 503 service unavailable HTTP code.")
.setSince("3.6")
.setChangelog(
+ new Change(V_2025_3, format(NEW_FACET_ADDED_MESSAGE, PARAM_OWASP_MOBILE_TOP_10_2024)),
+ new Change(V_2025_3, format(NEW_PARAM_ADDED_MESSAGE, PARAM_OWASP_MOBILE_TOP_10_2024)),
new Change("10.8", "The response fields 'severity' and 'type' are not deprecated anymore.."),
new Change("10.8", "The fields 'severity' and 'type' are not deprecated anymore."),
new Change("10.8", format("The parameters '%s' and '%s' are not deprecated anymore.", PARAM_SEVERITIES, PARAM_TYPES)),
@@ -284,7 +288,7 @@ public class SearchAction implements IssuesWsAction {
"api/hotspots"),
new Change("8.2", "response field 'fromHotspot' has been deprecated and is no more populated"),
new Change("8.2", "Status 'IN_REVIEW' for Security Hotspots has been deprecated"),
- new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, STATUS_IN_REVIEW,
+ new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, "IN_REVIEW",
STATUS_REVIEWED)),
new Change("7.8", "Security hotspots are returned by default"),
new Change("7.7", format("Value 'authors' in parameter '%s' is deprecated, please use '%s' instead", FACETS, PARAM_AUTHOR)),
@@ -379,6 +383,10 @@ public class SearchAction implements IssuesWsAction {
.setDescription("Comma-separated list of OWASP ASVS v4.0 categories.")
.setSince("9.7")
.setExampleValue("6,10.1.1");
+ action.createParam(PARAM_OWASP_MOBILE_TOP_10_2024)
+ .setDescription("Comma-separated list of OWASP Mobile Top 10 2024 lowercase categories.")
+ .setSince(V_2025_3)
+ .setPossibleValues("m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "m10");
action.createParam(PARAM_OWASP_TOP_10)
.setDescription("Comma-separated list of OWASP Top 10 2017 lowercase categories.")
.setSince("7.3")
@@ -621,6 +629,7 @@ public class SearchAction implements IssuesWsAction {
addMandatoryValuesToFacet(facets, PARAM_PCI_DSS_32, request.getPciDss32());
addMandatoryValuesToFacet(facets, PARAM_PCI_DSS_40, request.getPciDss40());
addMandatoryValuesToFacet(facets, PARAM_OWASP_ASVS_40, request.getOwaspAsvs40());
+ addMandatoryValuesToFacet(facets, PARAM_OWASP_MOBILE_TOP_10_2024, request.getOwaspMobileTop10For2024());
addMandatoryValuesToFacet(facets, PARAM_OWASP_TOP_10, request.getOwaspTop10());
addMandatoryValuesToFacet(facets, PARAM_OWASP_TOP_10_2021, request.getOwaspTop10For2021());
addMandatoryValuesToFacet(facets, PARAM_STIG_ASD_V5R3, request.getStigAsdV5R3());
@@ -710,6 +719,7 @@ public class SearchAction implements IssuesWsAction {
.setPciDss40(request.paramAsStrings(PARAM_PCI_DSS_40))
.setOwaspAsvsLevel(request.paramAsInt(PARAM_OWASP_ASVS_LEVEL))
.setOwaspAsvs40(request.paramAsStrings(PARAM_OWASP_ASVS_40))
+ .setOwaspMobileTop10For2024(request.paramAsStrings(PARAM_OWASP_MOBILE_TOP_10_2024))
.setOwaspTop10(request.paramAsStrings(PARAM_OWASP_TOP_10))
.setOwaspTop10For2021(request.paramAsStrings(PARAM_OWASP_TOP_10_2021))
.setStigAsdV5R3(request.paramAsStrings(PARAM_STIG_ASD_V5R3))
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java
index a5844238eb2..9414b31e895 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java
@@ -21,36 +21,23 @@ package org.sonar.server.measure.live;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import javax.annotation.CheckForNull;
-import org.slf4j.LoggerFactory;
-import org.sonar.api.config.Configuration;
import org.sonar.api.measures.Metric;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
-import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
-import org.sonar.db.measure.MeasureDto;
-import org.sonar.db.metric.MetricDto;
-import org.sonar.db.project.ProjectDto;
import org.sonar.server.es.Indexers;
import org.sonar.server.qualitygate.EvaluatedQualityGate;
-import org.sonar.server.qualitygate.QualityGate;
import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
import org.sonar.server.setting.ProjectConfigurationLoader;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.groupingBy;
-import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
public class LiveMeasureComputerImpl implements LiveMeasureComputer {
@@ -96,81 +83,36 @@ public class LiveMeasureComputerImpl implements LiveMeasureComputer {
return Optional.empty();
}
- BranchDto branch = loadBranch(dbSession, branchComponent);
- ProjectDto project = loadProject(dbSession, branch.getProjectUuid());
- Configuration config = projectConfigurationLoader.loadBranchConfiguration(dbSession, branch);
- QualityGate qualityGate = qGateComputer.loadQualityGate(dbSession, project, branch);
- MeasureMatrix matrix = loadMeasureMatrix(dbSession, components.getAllUuids(), qualityGate);
-
- treeUpdater.update(dbSession, lastAnalysis.get(), config, components, branch, matrix);
-
- Metric.Level previousStatus = loadPreviousStatus(dbSession, branchComponent);
- EvaluatedQualityGate evaluatedQualityGate = qGateComputer.refreshGateStatus(branchComponent, qualityGate, matrix, config);
- persistAndIndex(dbSession, matrix, branch);
-
- return Optional.of(new QGChangeEvent(project, branch, lastAnalysis.get(), config, previousStatus, () -> Optional.of(evaluatedQualityGate)));
- }
-
- private MeasureMatrix loadMeasureMatrix(DbSession dbSession, Set<String> componentUuids, QualityGate qualityGate) {
- Collection<String> metricKeys = getKeysOfAllInvolvedMetrics(qualityGate);
- Map<String, MetricDto> metricPerKey =
- dbClient.metricDao().selectByKeys(dbSession, metricKeys).stream().collect(Collectors.toMap(MetricDto::getKey, Function.identity()));
- List<MeasureDto> measures = dbClient.measureDao()
- .selectByComponentUuidsAndMetricKeys(dbSession, componentUuids, metricPerKey.keySet());
- return new MeasureMatrix(componentUuids, metricPerKey.values(), measures);
- }
-
- private void persistAndIndex(DbSession dbSession, MeasureMatrix matrix, BranchDto branch) {
- // persist the measures that have been created or updated
- Map<String, MeasureDto> measureDtoPerComponent = new HashMap<>();
- matrix.getChanged().sorted(MeasureMatrix.Measure.COMPARATOR)
- .filter(m -> m.getValue() != null)
- .forEach(m -> measureDtoPerComponent.compute(m.getComponentUuid(), (componentUuid, measureDto) -> {
- if (measureDto == null) {
- measureDto = new MeasureDto()
- .setComponentUuid(componentUuid)
- .setBranchUuid(m.getBranchUuid());
- }
- return measureDto.addValue(
- m.getMetricKey(),
- m.getValue()
- );
- }));
- measureDtoPerComponent.values().forEach(m -> dbClient.measureDao().insertOrUpdate(dbSession, m));
- projectIndexer.commitAndIndexBranches(dbSession, singleton(branch), Indexers.BranchEvent.MEASURE_CHANGE);
- }
-
- @CheckForNull
- private Metric.Level loadPreviousStatus(DbSession dbSession, ComponentDto branchComponent) {
- return dbClient.measureDao().selectByComponentUuid(dbSession, branchComponent.uuid())
- .map(m -> m.getString(ALERT_STATUS_KEY))
- .map(m -> {
- try {
- return Metric.Level.valueOf(m);
- } catch (IllegalArgumentException e) {
- LoggerFactory.getLogger(LiveMeasureComputerImpl.class).trace("Failed to parse value of metric '{}'", ALERT_STATUS_KEY, e);
- return null;
- }
- })
- .orElse(null);
- }
-
- private Set<String> getKeysOfAllInvolvedMetrics(QualityGate gate) {
- Set<String> metricKeys = new HashSet<>();
- for (Metric<?> metric : formulaFactory.getFormulaMetrics()) {
- metricKeys.add(metric.getKey());
- }
- metricKeys.addAll(qGateComputer.getMetricsRelatedTo(gate));
- return metricKeys;
- }
-
- private BranchDto loadBranch(DbSession dbSession, ComponentDto branchComponent) {
- return dbClient.branchDao().selectByUuid(dbSession, branchComponent.uuid())
- .orElseThrow(() -> new IllegalStateException("Branch not found: " + branchComponent.uuid()));
- }
-
- private ProjectDto loadProject(DbSession dbSession, String uuid) {
- return dbClient.projectDao().selectByUuid(dbSession, uuid)
- .orElseThrow(() -> new IllegalStateException("Project not found: " + uuid));
+ LiveMeasureUpdaterWorkflow liveMeasureUpdaterWorkflow = LiveMeasureUpdaterWorkflow.build(
+ dbClient,
+ dbSession,
+ branchComponent,
+ projectConfigurationLoader,
+ qGateComputer);
+
+ Set<String> metricKeys = liveMeasureUpdaterWorkflow.getKeysOfAllInvolvedMetrics(formulaFactory);
+ MeasureMatrix matrix = liveMeasureUpdaterWorkflow.buildMeasureMatrix(
+ metricKeys,
+ components.getAllUuids());
+
+ treeUpdater.update(
+ dbSession,
+ lastAnalysis.get(),
+ liveMeasureUpdaterWorkflow.getConfig(),
+ components,
+ liveMeasureUpdaterWorkflow.getBranchDto(),
+ matrix);
+
+ Metric.Level previousStatus = liveMeasureUpdaterWorkflow.loadPreviousStatus();
+ EvaluatedQualityGate evaluatedQualityGate = liveMeasureUpdaterWorkflow.updateQualityGateMeasures(matrix);
+
+ projectIndexer.commitAndIndexBranches(dbSession, singleton(liveMeasureUpdaterWorkflow.getBranchDto()), Indexers.BranchEvent.MEASURE_CHANGE);
+
+ return Optional.of(new QGChangeEvent(
+ liveMeasureUpdaterWorkflow.getProjectDto(),
+ liveMeasureUpdaterWorkflow.getBranchDto(),
+ lastAnalysis.get(),
+ liveMeasureUpdaterWorkflow.getConfig(),
+ previousStatus, () -> Optional.of(evaluatedQualityGate)));
}
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflow.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflow.java
new file mode 100644
index 00000000000..26906ab9c35
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureUpdaterWorkflow.java
@@ -0,0 +1,204 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.measure.live;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.measure.MeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+import org.sonar.server.qualitygate.QualityGate;
+import org.sonar.server.setting.ProjectConfigurationLoader;
+
+import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
+
+/**
+ * This class breaks apart the various steps required to update an
+ * existing quality gate's measures with new ones:
+ * <ul>
+ * <li>
+ * Get related metric keys
+ * <ul><li>After this step is where you would inject additional keys</li></ul>
+ * </li>
+ * <li>
+ * Build a "measure matrix", a special hash table that contains the original measures mapped to the related metric keys
+ * <ul><li>This matrix is then updated with new measure values</li></ul>
+ * </li>
+ * <li>
+ * Load the previous quality gate status
+ * </li>
+ * <li>
+ * Update the quality gate's measures, persisting them to the database
+ * </li>
+ * </ul>
+ */
+public class LiveMeasureUpdaterWorkflow {
+ private final Configuration config;
+ private final QualityGate qualityGate;
+ private final DbClient dbClient;
+ private final DbSession dbSession;
+ private final LiveQualityGateComputer qualityGateComputer;
+ private final Dtos dtos;
+
+ private LiveMeasureUpdaterWorkflow(
+ Dtos dtos,
+ DbClient dbClient,
+ DbSession dbSession,
+ Configuration config,
+ QualityGate qualityGate,
+ LiveQualityGateComputer qualityGateComputer) {
+ this.dtos = dtos;
+ this.config = config;
+ this.qualityGate = qualityGate;
+ this.dbClient = dbClient;
+ this.dbSession = dbSession;
+ this.qualityGateComputer = qualityGateComputer;
+ }
+
+ public static LiveMeasureUpdaterWorkflow build(
+ DbClient dbClient,
+ DbSession dbSession,
+ ComponentDto branchComponentDto,
+ ProjectConfigurationLoader projectConfigurationLoader,
+ LiveQualityGateComputer qualityGateComputer) {
+ BranchDto branchDto = loadBranch(dbClient, dbSession, branchComponentDto);
+ ProjectDto projectDto = loadProject(dbClient, dbSession, branchDto.getProjectUuid());
+ Configuration config = projectConfigurationLoader.loadBranchConfiguration(dbSession, branchDto);
+ QualityGate qualityGate = qualityGateComputer.loadQualityGate(dbSession, projectDto, branchDto);
+
+ return new LiveMeasureUpdaterWorkflow(
+ new Dtos(branchComponentDto, branchDto, projectDto),
+ dbClient,
+ dbSession,
+ config,
+ qualityGate,
+ qualityGateComputer);
+ }
+
+ private static BranchDto loadBranch(
+ DbClient dbClient,
+ DbSession dbSession,
+ ComponentDto branchComponent) {
+ return dbClient.branchDao().selectByUuid(dbSession, branchComponent.uuid())
+ .orElseThrow(() -> new IllegalStateException("Branch not found: " + branchComponent.uuid()));
+ }
+
+ private static ProjectDto loadProject(
+ DbClient dbClient,
+ DbSession dbSession,
+ String uuid) {
+ return dbClient.projectDao().selectByUuid(dbSession, uuid)
+ .orElseThrow(() -> new IllegalStateException("Project not found: " + uuid));
+ }
+
+ public BranchDto getBranchDto() {
+ return dtos.branchDto;
+ }
+
+ public ProjectDto getProjectDto() {
+ return dtos.projectDto;
+ }
+
+ public Configuration getConfig() {
+ return config;
+ }
+
+ public EvaluatedQualityGate updateQualityGateMeasures(MeasureMatrix matrix) {
+ var result = qualityGateComputer.refreshGateStatus(
+ dtos.branchComponentDto,
+ qualityGate,
+ matrix,
+ config);
+
+ persistUpdatedMeasures(matrix);
+
+ return result;
+ }
+
+ public Set<String> getKeysOfAllInvolvedMetrics(MeasureUpdateFormulaFactory formulaFactory) {
+ Set<String> metricKeys = new HashSet<>();
+ for (Metric<?> metric : formulaFactory.getFormulaMetrics()) {
+ metricKeys.add(metric.getKey());
+ }
+ metricKeys.addAll(qualityGateComputer.getMetricsRelatedTo(qualityGate));
+
+ return metricKeys;
+ }
+
+ public MeasureMatrix buildMeasureMatrix(
+ Collection<String> metricKeys,
+ Set<String> branchUuids) {
+ Map<String, MetricDto> metricPerKey = dbClient.metricDao().selectByKeys(dbSession, metricKeys).stream().collect(Collectors.toMap(MetricDto::getKey, Function.identity()));
+ List<MeasureDto> measures = dbClient.measureDao()
+ .selectByComponentUuidsAndMetricKeys(dbSession, branchUuids, metricPerKey.keySet());
+ return new MeasureMatrix(branchUuids, metricPerKey.values(), measures);
+ }
+
+ private void persistUpdatedMeasures(MeasureMatrix matrix) {
+ // persist the measures that have been created or updated
+ Map<String, MeasureDto> measureDtoPerComponent = new HashMap<>();
+ matrix.getChanged().sorted(MeasureMatrix.Measure.COMPARATOR)
+ .filter(m -> m.getValue() != null)
+ .forEach(m -> measureDtoPerComponent.compute(m.getComponentUuid(), (componentUuid, measureDto) -> {
+ if (measureDto == null) {
+ measureDto = new MeasureDto()
+ .setComponentUuid(componentUuid)
+ .setBranchUuid(m.getBranchUuid());
+ }
+ return measureDto.addValue(
+ m.getMetricKey(),
+ m.getValue());
+ }));
+ measureDtoPerComponent.values().forEach(m -> dbClient.measureDao().insertOrUpdate(dbSession, m));
+ }
+
+ @CheckForNull
+ public Metric.Level loadPreviousStatus() {
+ return dbClient.measureDao().selectByComponentUuid(dbSession, dtos.branchDto.getUuid())
+ .map(m -> m.getString(ALERT_STATUS_KEY))
+ .map(m -> {
+ try {
+ return Metric.Level.valueOf(m);
+ } catch (IllegalArgumentException e) {
+ LoggerFactory.getLogger(LiveMeasureUpdaterWorkflow.class).trace("Failed to parse value of metric '{}'", ALERT_STATUS_KEY, e);
+ return null;
+ }
+ })
+ .orElse(null);
+ }
+
+ private record Dtos(ComponentDto branchComponentDto, BranchDto branchDto, ProjectDto projectDto) {
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java
index da4df11d0fa..fc82532d94e 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java
@@ -51,17 +51,17 @@ import static java.util.Objects.requireNonNull;
* <li>the refreshed values</li>
* </ul>
*/
-class MeasureMatrix {
+public class MeasureMatrix {
// component uuid -> metric key -> measure
private final Table<String, String, MeasureCell> table;
private final Map<String, MetricDto> metricsByKeys = new HashMap<>();
- MeasureMatrix(Collection<ComponentDto> components, Collection<MetricDto> metrics, List<MeasureDto> dbMeasures) {
+ public MeasureMatrix(Collection<ComponentDto> components, Collection<MetricDto> metrics, List<MeasureDto> dbMeasures) {
this(components.stream().map(ComponentDto::uuid).collect(Collectors.toSet()), metrics, dbMeasures);
}
- MeasureMatrix(Set<String> componentUuids, Collection<MetricDto> metrics, List<MeasureDto> dbMeasures) {
+ public MeasureMatrix(Set<String> componentUuids, Collection<MetricDto> metrics, List<MeasureDto> dbMeasures) {
for (MetricDto metric : metrics) {
this.metricsByKeys.put(metric.getKey(), metric);
}
@@ -86,19 +86,19 @@ class MeasureMatrix {
return cell == null ? Optional.empty() : Optional.of(cell.measure);
}
- void setValue(ComponentDto component, String metricKey, double value) {
+ public void setValue(ComponentDto component, String metricKey, double value) {
changeCell(component, metricKey, m -> m.setValue(scale(getMetric(metricKey), value)));
}
- void setValue(ComponentDto component, String metricKey, Rating value) {
+ public void setValue(ComponentDto component, String metricKey, Rating value) {
changeCell(component, metricKey, m -> m.setValue((double) value.getIndex()));
}
- void setValue(ComponentDto component, String metricKey, @Nullable String data) {
+ public void setValue(ComponentDto component, String metricKey, @Nullable String data) {
changeCell(component, metricKey, m -> m.setValue(data));
}
- Stream<Measure> getChanged() {
+ public Stream<Measure> getChanged() {
return table.values().stream()
.filter(Objects::nonNull)
.filter(MeasureCell::isChanged)
@@ -145,8 +145,8 @@ class MeasureMatrix {
}
}
- static class Measure {
- static final Comparator<Measure> COMPARATOR = Comparator
+ public static class Measure {
+ public static final Comparator<Measure> COMPARATOR = Comparator
.comparing((Measure m) -> m.componentUuid)
.thenComparing(m -> m.metricDto.getKey());
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/ActiveVersionEvaluator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/ActiveVersionEvaluator.java
index 2f045a824ed..01e86f3a35e 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/ActiveVersionEvaluator.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/ActiveVersionEvaluator.java
@@ -19,12 +19,12 @@
*/
package org.sonar.server.platform.ws;
-import com.google.common.collect.Lists;
+import java.time.LocalDate;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
-import java.util.List;
import java.util.SortedSet;
+import org.sonar.api.internal.MetadataLoader;
import org.sonar.api.utils.System2;
import org.sonar.core.platform.SonarQubeVersion;
import org.sonar.updatecenter.common.Product;
@@ -67,11 +67,11 @@ public class ActiveVersionEvaluator {
return initialLtaReleaseDate.after(c.getTime());
} else {
- return compareWithoutPatchVersion(installedVersion, findPreviousReleaseIgnoringPatch(allReleases).getVersion()) >= 0;
+ // if installed version is not LTA or past LTA, check if it is still supported
+ return LocalDate.now().isBefore(LocalDate.parse(MetadataLoader.loadSqVersionEol(system2)));
}
}
-
private static int compareWithoutPatchVersion(Version v1, Version v2) {
return COMPARATOR.compare(v1, v2);
}
@@ -85,24 +85,4 @@ public class ActiveVersionEvaluator {
));
}
- private static Release findPreviousReleaseIgnoringPatch(SortedSet<Release> releases) {
- if (!releases.isEmpty()) {
- Release refRelease = releases.last();
- int patchesOfRefRelease = 0;
- List<Release> sublist = Lists.reverse(releases.stream().toList());
- for (Release release : sublist) {
- int versionComparison = compareWithoutPatchVersion(release.getVersion(), refRelease.getVersion());
- if (versionComparison < 0) {
- return release;
- } else if (versionComparison == 0) {
- patchesOfRefRelease++;
- }
- }
- // if all releases have the same version, return the last one
- if (patchesOfRefRelease == releases.size()) {
- return refRelease;
- }
- }
- throw new IllegalStateException("Unable to find previous release in releases");
- }
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java
index dd20b9c78c8..048487d44ba 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java
@@ -106,7 +106,7 @@ public class BuiltInQProfileRepositoryImpl implements BuiltInQProfileRepository
.collect(Collectors.toSet());
checkState(languagesWithoutBuiltInQProfiles.isEmpty(), "The following languages have no built-in quality profiles: %s",
- languagesWithoutBuiltInQProfiles.isEmpty() ? "" : String.join("", languagesWithoutBuiltInQProfiles));
+ languagesWithoutBuiltInQProfiles.isEmpty() ? "" : String.join(", ", languagesWithoutBuiltInQProfiles));
}
private Map<String, Map<String, BuiltInQualityProfile>> validateAndClean(BuiltInQualityProfilesDefinition.Context context) {
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
index 407ffcf2f36..5c5b2c8b36c 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
@@ -41,6 +41,7 @@ import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.QUALIT
import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_DNA_BANNER;
import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_DNA_OPTIN_BANNER;
import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_DNA_TOUR;
+import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_ENABLE_SCA;
import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_NEW_MODES_BANNER;
import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_NEW_MODES_TOUR;
@@ -59,6 +60,7 @@ public class DismissNoticeAction implements UsersWsAction {
SHOW_DNA_OPTIN_BANNER("showDesignAndArchitectureOptInBanner"),
SHOW_DNA_BANNER("showDesignAndArchitectureBanner"),
SHOW_DNA_TOUR("showDesignAndArchitectureTour"),
+ SHOW_ENABLE_SCA("showEnableSca"),
;
private final String key;
@@ -99,12 +101,12 @@ public class DismissNoticeAction implements UsersWsAction {
return SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(noticesList);
}
-
@Override
public void define(WebService.NewController context) {
WebService.NewAction action = context.createAction("dismiss_notice")
.setDescription("Dismiss a notice for the current user. Silently ignore if the notice is already dismissed.")
- .setChangelog(new Change("25.4", printNewNotice(SHOW_DNA_OPTIN_BANNER, SHOW_DNA_BANNER, SHOW_DNA_TOUR)))
+ .setChangelog(new Change("2025.3", printNewNotice(SHOW_ENABLE_SCA)))
+ .setChangelog(new Change("2025.3", printNewNotice(SHOW_DNA_OPTIN_BANNER, SHOW_DNA_BANNER, SHOW_DNA_TOUR)))
.setChangelog(new Change("10.8", printNewNotice(SHOW_NEW_MODES_TOUR)))
.setChangelog(new Change("10.8", printNewNotice(SHOW_NEW_MODES_BANNER)))
.setChangelog(new Change("10.6", printNewNotice(ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE)))
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java
index 5b0d652caf6..811b8c10f54 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java
@@ -73,7 +73,7 @@ public class SearchAction implements UserGroupsWsAction {
public void define(NewController context) {
WebService.NewAction action = context.createAction("search")
.setDescription("Search for user groups.<br>" +
- "Requires the following permission: 'Administer System'.")
+ "Requires the following permission: 'Administer System'.")
.setHandler(this)
.setResponseExample(getClass().getResource("search-example.json"))
.setSince("5.2")
@@ -107,7 +107,8 @@ public class SearchAction implements UserGroupsWsAction {
try (DbSession dbSession = dbClient.openSession(false)) {
userSession.checkLoggedIn().checkIsSystemAdministrator();
- GroupSearchRequest groupSearchRequest = new GroupSearchRequest(request.param(Param.TEXT_QUERY), request.paramAsBoolean(MANAGED_PARAM), page, pageSize);
+ GroupSearchRequest groupSearchRequest = new GroupSearchRequest(request.param(Param.TEXT_QUERY), request.paramAsBoolean(MANAGED_PARAM), null, null,
+ page, pageSize);
SearchResults<GroupInformation> searchResults = groupService.search(dbSession, groupSearchRequest);
Set<String> groupUuids = extractGroupUuids(searchResults.searchResults());
@@ -136,7 +137,7 @@ public class SearchAction implements UserGroupsWsAction {
}
private static SearchWsResponse buildResponse(List<GroupInformation> groups, Map<String, Integer> userCountByGroup,
- Set<String> fields, Paging paging) {
+ Set<String> fields, Paging paging) {
SearchWsResponse.Builder responseBuilder = SearchWsResponse.newBuilder();
groups.forEach(group -> responseBuilder
.addGroups(toWsGroup(group.groupDto(), userCountByGroup.get(group.groupDto().getName()), group.isManaged(), fields, group.isDefault())));
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/batch/BatchIndexTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/batch/BatchIndexTest.java
index 902db3d5a81..39091a6dd9b 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/batch/BatchIndexTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/batch/BatchIndexTest.java
@@ -21,12 +21,14 @@ package org.sonar.server.batch;
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.CharUtils;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.platform.ServerFileSystem;
@@ -36,28 +38,28 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-public class BatchIndexTest {
+class BatchIndexTest {
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
+ @TempDir
+ Path temp;
private File jar;
- private ServerFileSystem fs = mock(ServerFileSystem.class);
+ private final ServerFileSystem fs = mock(ServerFileSystem.class);
- @Before
- public void prepare_fs() throws IOException {
- File homeDir = temp.newFolder();
- when(fs.getHomeDir()).thenReturn(homeDir);
+ @BeforeEach
+ void prepare_fs() throws IOException {
+ Path homeDir = Files.createTempDirectory(temp, "homeDir");
+ when(fs.getHomeDir()).thenReturn(homeDir.toFile());
- File batchDir = new File(homeDir, "lib/scanner");
- FileUtils.forceMkdir(batchDir);
- jar = new File(batchDir, "sonar-batch.jar");
- FileUtils.writeStringToFile(new File(batchDir, "sonar-batch.jar"), "foo");
+ Path batchDir = homeDir.resolve("lib/scanner");
+ Files.createDirectories(batchDir);
+ jar = batchDir.resolve("sonar-batch.jar").toFile();
+ FileUtils.writeByteArrayToFile(batchDir.resolve("sonar-batch.jar").toFile(), "foo".getBytes(StandardCharsets.UTF_8));
}
@Test
- public void get_index() {
+ void get_index() {
BatchIndex batchIndex = new BatchIndex(fs);
batchIndex.start();
@@ -68,7 +70,7 @@ public class BatchIndexTest {
}
@Test
- public void get_file() {
+ void get_file() {
BatchIndex batchIndex = new BatchIndex(fs);
batchIndex.start();
@@ -81,37 +83,35 @@ public class BatchIndexTest {
* /etc/passwd
*/
@Test
- public void check_location_of_file() {
- assertThatThrownBy(() -> {
- BatchIndex batchIndex = new BatchIndex(fs);
- batchIndex.start();
-
- batchIndex.getFile("../sonar-batch.jar");
- })
+ void check_location_of_file() {
+ BatchIndex batchIndex = new BatchIndex(fs);
+ batchIndex.start();
+ assertThatThrownBy(() -> batchIndex.getFile("../sonar-batch.jar"))
.isInstanceOf(NotFoundException.class)
.hasMessage("Bad filename: ../sonar-batch.jar");
}
@Test
- public void file_does_not_exist() {
- assertThatThrownBy(() -> {
- BatchIndex batchIndex = new BatchIndex(fs);
- batchIndex.start();
-
- batchIndex.getFile("other.jar");
- })
+ void file_does_not_exist() {
+ BatchIndex batchIndex = new BatchIndex(fs);
+ batchIndex.start();
+ assertThatThrownBy(() -> batchIndex.getFile("other.jar"))
.isInstanceOf(NotFoundException.class)
.hasMessage("Bad filename: other.jar");
}
@Test
- public void start_whenBatchDirDoesntExist_shouldThrow() throws IOException {
- File homeDir = temp.newFolder();
- when(fs.getHomeDir()).thenReturn(homeDir);
+ void start_whenBatchDirDoesntExist_shouldThrow() throws IOException {
+ Path homeDir = Files.createTempDirectory(temp, "homeDir");
+ when(fs.getHomeDir()).thenReturn(homeDir.toFile());
BatchIndex batchIndex = new BatchIndex(fs);
+
+ // Ensure that the file separator is correct based on the OS
+ String expectedMessage = format("%s%slib%sscanner folder not found",
+ homeDir.toFile().getAbsolutePath(), File.separator, File.separator);
assertThatThrownBy(batchIndex::start)
.isInstanceOf(IllegalStateException.class)
- .hasMessage(format("%s/lib/scanner folder not found", homeDir.getAbsolutePath()));
+ .hasMessage(expectedMessage);
}
}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java
index 1872f4f3589..14bfe6dae92 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java
@@ -20,26 +20,28 @@
package org.sonar.server.issue.ws.anticipatedtransition;
import java.io.IOException;
+import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.assertj.core.api.Assertions;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.sonar.api.rule.RuleKey;
import org.sonar.core.issue.AnticipatedTransition;
import static org.assertj.core.api.Assertions.assertThat;
-public class AnticipatedTransitionParserTest {
+class AnticipatedTransitionParserTest {
private static final String USER_UUID = "userUuid";
private static final String PROJECT_KEY = "projectKey";
+ public static final String REQUEST_WITH_TRANSITIONS_JSON = "request-with-transitions.json";
AnticipatedTransitionParser underTest = new AnticipatedTransitionParser();
@Test
- public void givenRequestBodyWithMultipleTransition_whenParse_thenAllTransitionsAreReturned() throws IOException {
+ void givenRequestBodyWithMultipleTransition_whenParse_thenAllTransitionsAreReturned() throws IOException, URISyntaxException {
// given
- String requestBody = readTestResourceFile("request-with-transitions.json");
+ String requestBody = readTestResourceFile();
// when
List<AnticipatedTransition> anticipatedTransitions = underTest.parse(requestBody, USER_UUID, PROJECT_KEY);
@@ -52,7 +54,7 @@ public class AnticipatedTransitionParserTest {
}
@Test
- public void givenRequestBodyWithNoTransitions_whenParse_ThenAnEmptyListIsReturned() {
+ void givenRequestBodyWithNoTransitions_whenParse_ThenAnEmptyListIsReturned() {
// given
String requestBody = "[]";
@@ -64,7 +66,7 @@ public class AnticipatedTransitionParserTest {
}
@Test
- public void givenRequestBodyWithInvalidJson_whenParse_thenExceptionIsThrown() {
+ void givenRequestBodyWithInvalidJson_whenParse_thenExceptionIsThrown() {
// given
String requestBody = "invalidJson";
@@ -75,7 +77,7 @@ public class AnticipatedTransitionParserTest {
}
@Test
- public void givenRequestBodyWithInvalidTransition_whenParse_thenExceptionIsThrown() throws IOException {
+ void givenRequestBodyWithInvalidTransition_whenParse_thenExceptionIsThrown() {
// given
String requestBodyWithInvalidTransition = """
[
@@ -124,8 +126,8 @@ public class AnticipatedTransitionParserTest {
"comment2"));
}
- private String readTestResourceFile(String fileName) throws IOException {
- return Files.readString(Path.of(getClass().getResource(fileName).getPath()));
+ private String readTestResourceFile() throws IOException, URISyntaxException {
+ return Files.readString(Path.of(getClass().getResource(AnticipatedTransitionParserTest.REQUEST_WITH_TRANSITIONS_JSON).toURI()));
}
}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/ActiveVersionEvaluatorTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/ActiveVersionEvaluatorTest.java
index 9e59be5b82b..eace45eb2a0 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/ActiveVersionEvaluatorTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/ActiveVersionEvaluatorTest.java
@@ -19,12 +19,14 @@
*/
package org.sonar.server.platform.ws;
+import java.time.LocalDate;
import java.util.Calendar;
-import java.util.Collections;
import java.util.SortedSet;
import java.util.TreeSet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.sonar.api.internal.MetadataLoader;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.System2;
import org.sonar.core.platform.SonarQubeVersion;
@@ -107,69 +109,29 @@ class ActiveVersionEvaluatorTest {
}
@Test
- void evaluateIfActiveVersion_whenNoReleasesFound_shouldThrowIllegalStateException() {
+ void evaluateIfActiveVersion_whenEOLDateIsAfterToday_shouldReturnActiveVersion() {
+ LocalDate today = LocalDate.now();
+ LocalDate tomorrow = today.plusDays(1);
- when(sonarQubeVersion.get()).thenReturn(parse("10.8.0"));
+ when(sonarQubeVersion.get()).thenReturn(parse("2025.4.0.12345"));
- when(sonar.getAllReleases(any())).thenReturn(Collections.emptySortedSet());
-
- assertThatThrownBy(() -> underTest.evaluateIfActiveVersion(updateCenter))
- .isInstanceOf(IllegalStateException.class)
- .hasMessageContaining("Unable to find previous release in releases");
+ try (MockedStatic<MetadataLoader> mocked = org.mockito.Mockito.mockStatic(MetadataLoader.class)) {
+ mocked.when(() -> MetadataLoader.loadSqVersionEol(system2)).thenReturn(tomorrow.toString());
+ assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
+ }
}
@Test
- void evaluateIfActiveVersion_whenInstalledVersionIsLatestMinusOne_shouldReturnVersionIsActive() {
- when(sonarQubeVersion.get()).thenReturn(parse("10.9"));
- when(updateCenter.getSonar().getAllReleases(any())).thenReturn(getReleases());
+ void evaluateIfActiveVersion_whenEOLDateIsBeforeToday_shouldReturnInactiveVersion() {
+ LocalDate today = LocalDate.now();
+ LocalDate yesterday = today.minusDays(1);
- assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
- }
-
- @Test
- void evaluateIfActiveVersion_whenInstalledVersionIsSnapshot_shouldReturnVersionIsActive() {
- when(sonarQubeVersion.get()).thenReturn(parse("10.11-SNAPSHOT"));
- when(updateCenter.getSonar().getAllReleases(any())).thenReturn(getReleases());
+ when(sonarQubeVersion.get()).thenReturn(parse("2025.4.0.12345"));
- assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
- }
-
- @Test
- void evaluateIfActiveVersion_whenInstalledVersionIsTheOnlyAvailableVersion_shouldReturnVersionIsActive() {
- TreeSet<Release> releases = new TreeSet<>();
- releases.add(new Release(sonar, Version.create("10.8.0.12345")));
-
- when(sonarQubeVersion.get()).thenReturn(parse("10.8.0.12345"));
- when(updateCenter.getSonar().getAllReleases(any())).thenReturn(releases);
-
- assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
- }
-
- @Test
- void evaluateIfActiveVersion_whenAvailableVersionsAreAllPatchesOfInstalledVersion_shouldReturnVersionIsActive() {
- TreeSet<Release> releases = new TreeSet<>();
- releases.add(new Release(sonar, Version.create("10.8.0.12345")));
- releases.add(new Release(sonar, Version.create("10.8.1.12346")));
- when(sonar.getAllReleases(any())).thenReturn(releases);
-
- when(sonarQubeVersion.get()).thenReturn(parse("10.8.0.12345"));
- when(updateCenter.getSonar().getAllReleases(any())).thenReturn(releases);
-
- assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
- }
-
- @Test
- void evaluateIfActiveVersion_whenAvailableVersionsHaveDifferentNamingScheme_shouldReturnVersionIsActive() {
- TreeSet<Release> releases = new TreeSet<>();
- releases.add(new Release(sonar, Version.create("10.8.0.12345")));
- releases.add(new Release(sonar, Version.create("10.8.1.12346")));
- releases.add(new Release(sonar, Version.create("2025.1.0.12347")));
- when(sonar.getAllReleases(any())).thenReturn(releases);
-
- when(sonarQubeVersion.get()).thenReturn(parse("10.8.0.12345"));
- when(updateCenter.getSonar().getAllReleases(any())).thenReturn(releases);
-
- assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isTrue();
+ try (MockedStatic<MetadataLoader> mocked = org.mockito.Mockito.mockStatic(MetadataLoader.class)) {
+ mocked.when(() -> MetadataLoader.loadSqVersionEol(system2)).thenReturn(yesterday.toString());
+ assertThat(underTest.evaluateIfActiveVersion(updateCenter)).isFalse();
+ }
}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java
new file mode 100644
index 00000000000..2f76e5f9435
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.qualityprofile.builtin;
+
+import org.junit.jupiter.api.Test;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.db.DbClient;
+import org.sonar.server.rule.ServerRuleFinder;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class BuiltInQProfileRepositoryImplTest {
+ @Test
+ void initializationWithoutQualityProfiles() {
+ DbClient dbClient = mock(DbClient.class);
+ ServerRuleFinder ruleFinder = mock(ServerRuleFinder.class);
+ Languages languages = mock(Languages.class);
+ Language java = mock(Language.class);
+ Language kotlin = mock(Language.class);
+
+ when(languages.all()).thenReturn(new Language[]{ java, kotlin });
+ when(java.getKey()).thenReturn("java");
+ when(kotlin.getKey()).thenReturn("kotlin");
+
+ BuiltInQProfileRepositoryImpl repository = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, languages);
+
+ assertThatCode(repository::initialize).hasMessage("The following languages have no built-in quality profiles: java, kotlin");
+ }
+}
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index cb5cb05bc90..795ba588669 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -58,6 +58,7 @@ import org.sonar.core.language.LanguagesProvider;
import org.sonar.core.metric.SoftwareQualitiesMetrics;
import org.sonar.core.platform.PlatformEditionProvider;
import org.sonar.core.platform.SpringComponentContainer;
+import org.sonar.core.scadata.DefaultScaDataSourceImpl;
import org.sonar.server.ai.code.assurance.AiCodeAssuranceEntitlement;
import org.sonar.server.ai.code.assurance.NoOpAiCodeAssuranceVerifier;
import org.sonar.server.almintegration.ws.AlmIntegrationsWSModule;
@@ -71,6 +72,7 @@ import org.sonar.server.authentication.DefaultAdminCredentialsVerifierImpl;
import org.sonar.server.authentication.DefaultAdminCredentialsVerifierNotificationHandler;
import org.sonar.server.authentication.DefaultAdminCredentialsVerifierNotificationTemplate;
import org.sonar.server.authentication.LogOAuthWarning;
+import org.sonar.server.authentication.HardcodedActiveTimeoutProvider;
import org.sonar.server.authentication.ws.AuthenticationWsModule;
import org.sonar.server.badge.ws.ProjectBadgesWsModule;
import org.sonar.server.batch.BatchWsModule;
@@ -207,6 +209,7 @@ import org.sonar.server.platform.telemetry.TelemetrySubportfolioSelectionModePro
import org.sonar.server.platform.telemetry.TelemetryUserEnabledProvider;
import org.sonar.server.platform.telemetry.TelemetryVersionProvider;
import org.sonar.server.platform.web.ActionDeprecationLoggerInterceptor;
+import org.sonar.server.platform.web.NoCacheFilter;
import org.sonar.server.platform.web.SonarQubeIdeConnectionFilter;
import org.sonar.server.platform.web.WebServiceFilter;
import org.sonar.server.platform.web.WebServiceReroutingFilter;
@@ -423,6 +426,7 @@ public class PlatformLevel4 extends PlatformLevel {
new WebServicesWsModule(),
SonarQubeIdeConnectionFilter.class,
WebServiceFilter.class,
+ NoCacheFilter.class,
WebServiceReroutingFilter.class,
// localization
@@ -446,6 +450,7 @@ public class PlatformLevel4 extends PlatformLevel {
DefaultAdminCredentialsVerifierImpl.class,
DefaultAdminCredentialsVerifierNotificationTemplate.class,
DefaultAdminCredentialsVerifierNotificationHandler.class,
+ HardcodedActiveTimeoutProvider.class,
// users
UserSessionFactoryImpl.class,
@@ -740,7 +745,10 @@ public class PlatformLevel4 extends PlatformLevel {
MainCollector.class,
- PluginsRiskConsentFilter.class);
+ PluginsRiskConsentFilter.class,
+
+ // sca-provided capabilities
+ DefaultScaDataSourceImpl.class);
// system info
add(new SystemInfoWriterModule(getWebServer()));
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java
index d81006a91e3..4ef6d79166e 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java
@@ -26,6 +26,7 @@ import org.sonar.server.platform.db.migration.AutoDbMigration;
import org.sonar.server.platform.db.migration.DatabaseMigrationImpl;
import org.sonar.server.platform.db.migration.MigrationEngineModule;
import org.sonar.server.platform.db.migration.NoopDatabaseMigrationImpl;
+import org.sonar.server.platform.web.NoCacheFilter;
import org.sonar.server.platform.web.WebServiceFilter;
import org.sonar.server.platform.ws.IndexAction;
import org.sonar.server.platform.ws.L10nWs;
@@ -59,6 +60,7 @@ public class PlatformLevelSafeMode extends PlatformLevel {
SafeModeUserSession.class,
WebServiceEngine.class,
WebServiceFilter.class,
+ NoCacheFilter.class,
// Monitoring
ServerMonitoringMetrics.class);
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/NoCacheFilter.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/NoCacheFilter.java
new file mode 100644
index 00000000000..268bafd3aed
--- /dev/null
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/NoCacheFilter.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.web;
+
+import org.sonar.api.server.http.HttpRequest;
+import org.sonar.api.server.http.HttpResponse;
+import org.sonar.api.web.FilterChain;
+import org.sonar.api.web.HttpFilter;
+import java.io.IOException;
+import org.sonar.api.web.UrlPattern;
+
+public class NoCacheFilter extends HttpFilter {
+
+ @Override
+ public void doFilter(HttpRequest httpRequest, HttpResponse httpResponse, FilterChain filterChain) throws IOException {
+ httpResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ filterChain.doFilter(httpRequest, httpResponse);
+ }
+
+ /**
+ * The Cache-Control for API v1 is handled in the org.sonar.server.ws.ServletResponse
+ */
+ @Override
+ public UrlPattern doGetPattern() {
+ return UrlPattern.builder()
+ .includes("/api/v2/*")
+ .build();
+ }
+}
diff --git a/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/NoCacheFilterTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/NoCacheFilterTest.java
new file mode 100644
index 00000000000..18b54291215
--- /dev/null
+++ b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/NoCacheFilterTest.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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.sonar.server.platform.web;
+
+import org.sonar.api.server.http.HttpRequest;
+import org.sonar.api.server.http.HttpResponse;
+import org.sonar.api.web.FilterChain;
+import org.junit.Test;
+import org.sonar.api.web.UrlPattern;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class NoCacheFilterTest {
+
+ private final NoCacheFilter filter = new NoCacheFilter();
+
+ @Test
+ public void doGetPattern_whenAPIv2_patternMatches() {
+ UrlPattern urlPattern = filter.doGetPattern();
+
+ assertThat(urlPattern.matches("/api/v2/whatever")).isTrue();
+ }
+
+ @Test
+ public void doGetPattern_whenAPIv1_patternDoesNotMatch() {
+ UrlPattern urlPattern = filter.doGetPattern();
+
+ assertThat(urlPattern.matches("/api/whatever")).isFalse();
+ }
+
+ @Test
+ public void doFilter_setResponseHeader() throws Exception{
+ HttpResponse response = mock();
+ HttpRequest request = mock();
+ FilterChain chain = mock();
+
+ filter.doFilter(request, response, chain);
+ verify(response).setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ }
+}