diff --git a/Changelog.md b/Changelog.md index 6a24e60ca4..0cf2349b89 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,7 @@ ## v3.9.14 - Update cargo strategy to parse new `cargo metadata` format for cargo >= 1.77.0 ([#1416](https://github.com/fossas/fossa-cli/pull/1416)). - `fossa release-group`: Add command to create a FOSSA release group release (`fossa release-group create-release`) [#1409](https://github.com/fossas/fossa-cli/pull/1409). +- `fossa project`: Adds commands to interact with FOSSA projects (`fossa project edit`) [#1394](https://github.com/fossas/fossa-cli/pull/1395). ## v3.9.13 - Support GIT dependencies in Bundler projects ([#1403](https://github.com/fossas/fossa-cli/pull/1403/files)) diff --git a/docs/README.md b/docs/README.md index e782d3aa63..6918a26336 100644 --- a/docs/README.md +++ b/docs/README.md @@ -134,6 +134,7 @@ Concept guides explain the nuances behind how basic FOSSA primitives work. If yo - [`fossa snippets`](./references/subcommands/snippets.md): Analyze snippets of a project and check if they exist in other open source projects FOSSA knows about. - [`fossa test`](./references/subcommands/test.md): View the results of the most recent scan of a project. - [`fossa release-group`](./references/subcommands/release-group.md): Interact with FOSSA release groups. +- [`fossa project`](./references/subcommands/project.md): Interact with FOSSA projects. #### Configuration diff --git a/docs/references/files/fossa-yml.md b/docs/references/files/fossa-yml.md index 94165e2095..2161d64af5 100644 --- a/docs/references/files/fossa-yml.md +++ b/docs/references/files/fossa-yml.md @@ -12,9 +12,13 @@ server: https://app.fossa.com apiKey: a1b2c3 project: + locator: custom+1/github.com/fossas/fossa-cli id: github.com/fossas/fossa-cli name: fossa-cli team: cli-team + teams: + - cli-team-1 + - cli-team-2 policy: custom-cli-policy link: fossa.com url: github.com/fossas/fossa-cli @@ -123,6 +127,11 @@ The project fields allow you to configure settings for the project you are inter > Note: `name`, `team`, `policy`, `link`, and `jiraProjectKey` can only be set when creating a project (running `fossa analyze` for the first time). Otherwise, they will be silently ignored (we would like to make this a visible warning in the future). +#### `project.locator:` +The project Locator defines a unique ID that the FOSSA API will use to reference this project within FOSSA. The project locator can be found in the UI on the project `Settings` page listed as the `Project Locator` underneath the `Project Title` setting. + + + #### `project.id:` The project ID defines an ID that is used to reference a project within your FOSSA organization. The project ID is a specific portion of the project locator and can be found in the UI on the project `Settings` page listed as the "Project Locator" underneath the "Project Title" setting. For example, if the "Project Locator" value of `custom+1/foo` is provided in the FOSSA UI, use `foo` for the `project.id`. @@ -144,6 +153,12 @@ The name field sets the projects visible name in the FOSSA dashboard. By default #### `project.team:` The name of the team in your FOSSA organization to associate this project with. +#### `project.teams:` +The name of the teams in your FOSSA organization to associate this project with. + +>NOTE: + Currently, ONLY `fossa project edit` utilizes this field. Use [fossa project edit](../subcommands/project/edit.md) to add a project to all teams in the list. + #### `project.policy:` The name of the policy in your FOSSA organization to associate this project with. Mutually excludes `project.policyId`. diff --git a/docs/references/files/fossa-yml.v3.schema.json b/docs/references/files/fossa-yml.v3.schema.json index 1f28f453ff..47c209563b 100644 --- a/docs/references/files/fossa-yml.v3.schema.json +++ b/docs/references/files/fossa-yml.v3.schema.json @@ -7,10 +7,15 @@ "type": "object", "description": "The project fields allow you to configure settings for the project you are interacting with through the FOSSA API.", "properties": { + "locator": { + "type": "string", + "minLength": 1, + "description": "The project Locator defines a unique ID that the FOSSA API will use to reference this project within FOSSA. The project locator can be found in the UI on the project `Settings` page listed as the `Project Locator` underneath the `Project Title` setting." + }, "id": { "type": "string", "minLength": 1, - "description": "The project ID defines a unique ID that the FOSSA API will use to reference this project. The project ID can be found in the UI on the project settings page listed as the `Project Locator` underneath the `Project Title` setting.\n\nBy default, it will use git remote origin url as project id if it's git repository. If it does not recognize version control system (vcs), project directory's name will be used." + "description": "The project ID defines a unique ID that the FOSSA API will use to reference this project within your organization. The project ID is a specific portion of the project locator and can be found in the UI on the project `Settings` page listed as the `Project Locator` underneath the `Project Title` setting.\n\nBy default, it will use git remote origin url as project id if it's git repository. If it does not recognize version control system (vcs), project directory's name will be used." }, "name": { "type": "string", @@ -22,6 +27,13 @@ "minLength": 1, "description": "The name of the team in your FOSSA organization to associate this project with." }, + "teams": { + "type": "array", + "description": "A list of team names in your FOSSA organization to associate this project with.", + "items": { + "type": "string" + } + }, "policy": { "type": "string", "minLength": 1, diff --git a/docs/references/subcommands/project.md b/docs/references/subcommands/project.md new file mode 100644 index 0000000000..56fb21ae96 --- /dev/null +++ b/docs/references/subcommands/project.md @@ -0,0 +1,27 @@ +## `fossa project` + +This `fossa project` subcommand allows users to interact with FOSSA projects. + +It has the following subcommand: + +- [`fossa project edit`](./project/edit.md) + +See the pages linked above for more details. + +### `fossa project edit` + +Edits a FOSSA project's settings and configurations. + +Example: + +```bash +fossa project edit --project-locator custom+1/example --title example-title --project-url github.com/fossas/fossa-cli --jira-project-key example-jira-key --link fossa.com --team example-team-1 --team example-team-2 --policy example-policy --project-label example-label-1 --project-label example-label-2 +``` + +### F.A.Q. + +1. Where can I find my project locator? + +The project Locator defines a unique ID that the FOSSA API will use to reference this project within FOSSA. The project locator can be found in the UI on the project `Settings` page listed as the "Project Locator" underneath the "Project Title" setting. + + diff --git a/docs/references/subcommands/project/edit.md b/docs/references/subcommands/project/edit.md new file mode 100644 index 0000000000..8ea3d727b9 --- /dev/null +++ b/docs/references/subcommands/project/edit.md @@ -0,0 +1,113 @@ +## `fossa project edit` + +`fossa project edit` allows you to edit a FOSSA project's settings and configuration. + +## Options + +Argument | Required | Description +-----------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------- +`--config` / `-c` | No | The to your path to your `.fossa.yml`. +`--project-locator` | Yes | The project Locator defines a unique ID that the FOSSA API will use to reference this project within FOSSA. The project locator can be found in the UI on the project `Settings` page listed as the "Project Locator" underneath the "Project Title" setting. +`--project-id` | Yes | The project ID defines an ID that is used to reference a project within your FOSSA organization. The project ID is a specific portion of the project locator and can be found in the UI on the project `Settings` page listed as the "Project Locator" underneath the "Project Title" setting. For example, if the "Project Locator" value of `custom+1/foo` is provided in the FOSSA UI, use `foo`. Project ID defaults to the .git/config file or project's remote "origin" URL (Git), "Repository Root" obtained using 'svn info' (SVN), or the name of the project's directory (No VCS), if project ID wasn't explicityly set during project creation. +`--title` / `-t` | No | The title of the FOSSA project. +`--project-url` | No | The url of the project's repository. +`--jira-project-key` / `-j` | No | The JIRA project key to associate to the FOSSA project. +`--link` / `-L` | No | A link to attach to the FOSSA project. +`--team` / `T` | No | The name of the team that will be associated with the FOSSA project. Specify multiple options by providing this argument multiple times. +`--project-label` | No | The labels associated with the FOSSA project. Assign up to 5 labels for a project. Specify multiple options by providing this argument multiple times. + +> NOTE: The arguments listed as `Required` need to be provided through CLI options OR through your `.fossa.yml` configuration. + +> NOTE: Either project ID OR project locator needs to be set. Project ID takes precedence over project locator. For more details on the differences between project ID and project locator refer to [documentation](../../files/fossa-yml.md#what-is-the-difference-between-project-id-and-project-locator). + +## .fossa.yml Configuration + +All of the previously mentioned CLI options can be provided through a `.fossa.yml`. Refer to [fossa configuration](../../files/fossa-yml.md) to set up your `.fossa.yml`. + +> NOTE: CLI options take precedence over the configurations in `.fossa.yml`. + +## `fossa project edit` usage and guidance + +### Updating a project's labels + +When updating project labels through `fossa project edit`, the transaction is all or nothing. This means that the project labels specified through this command will overwrite the existing labels that are associated with the project. Be sure to include all the labels that you want to be associated with the project, even if some labels are already currently set. + +### Updating the teams associated with a project + +Currently, `fossa project edit` only supports adding a project to the teams that are specified through the command. There will be support to remove a project from the provided teams in the future. + +Providing teams for `fossa project edit` takes the following precdence: + +1. CLI options - Adds the project to teams (1 or many) +2. `project.teams` in `.fossa.yml` - Adds the project to teams (1 or many) +3. `project.team` in `.fossa.yml` - Adds the project to a team + +There is support for `project.team` due to backwards compatability as `project.teams` is a newly added field in `.fossa.yml`. + +## Example + +### Project locator example +Given a project with project locator: `custom+1/example`, the following command: + +- Sets the project's title to `example-title` +- Set the the project's url to `github.com/fossas/fossa-cli` +- Sets the project's JIRA key to `example-jira-key` +- Attaches link: `fossa.com` to the project +- Adds the project to teams: `example-team-1`, `example-team-2` +- Attaches policy: `example-policy` to the project +- Attaches labels: `example-label-1` , `example-label-2` to the project + +```bash +fossa project edit --project-locator custom+1/example --title example-title --project-url github.com/fossas/fossa-cli --jira-project-key example-jira-key --link fossa.com --team example-team --team example-team-2 --policy example-policy --project-label example-label-1 --project-label example-label-2 +``` + +Similarly, you can you achieve the same result by running the following command with the given `.fossa.yml` configuration: + +```bash +fossa project edit --config /path/to/config +``` + +```yaml +project: + locator: custom+1/example + name: example-title + teams: + - example-team-1 + - example-team-2 + policy: example-policy + link: fossa.com + url: github.com/fossas/fossa-cli + jiraProjectKey: example-jira-key + labels: + - example-label-1 + - example-label-2 +``` + +### Project ID example +Achieve the same result as defined above (for projects created through the CLI) using project ID: + +```bash +fossa project edit --project-id example --title example-title --project-url github.com/fossas/fossa-cli --jira-project-key example-jira-key --link fossa.com --team example-team --team example-team-2 --policy example-policy --project-label example-label-1 --project-label example-label-2 +``` + +Similarly, you can you achieve the same result by running the following command with the given `.fossa.yml` configuration: + +```bash +fossa project edit --config /path/to/config +``` + +```yaml +project: + id: example + name: example-title + teams: + - example-team-1 + - example-team-2 + policy: example-policy + link: fossa.com + url: github.com/fossas/fossa-cli + jiraProjectKey: example-jira-key + labels: + - example-label-1 + - example-label-2 +``` diff --git a/spectrometer.cabal b/spectrometer.cabal index 74a973e1db..8e887bdc1b 100644 --- a/spectrometer.cabal +++ b/spectrometer.cabal @@ -189,6 +189,7 @@ library App.Fossa.Analyze.Upload App.Fossa.API.BuildLink App.Fossa.API.BuildWait + App.Fossa.ApiUtils App.Fossa.ArchiveUploader App.Fossa.BinaryDeps App.Fossa.BinaryDeps.Jar @@ -205,6 +206,8 @@ library App.Fossa.Config.LicenseScan App.Fossa.Config.LinkUserBinaries App.Fossa.Config.ListTargets + App.Fossa.Config.Project + App.Fossa.Config.Project.Edit App.Fossa.Config.ReleaseGroup App.Fossa.Config.ReleaseGroup.AddProjects App.Fossa.Config.ReleaseGroup.Common @@ -238,6 +241,8 @@ library App.Fossa.ManualDeps App.Fossa.PathDependency App.Fossa.PreflightChecks + App.Fossa.Project + App.Fossa.Project.Edit App.Fossa.ProjectInference App.Fossa.Reachability.Gradle App.Fossa.Reachability.Jar @@ -379,6 +384,7 @@ library Effect.Grapher Effect.Logger Effect.ReadFS + Fossa.API.CoreTypes Fossa.API.Types Graphing Graphing.Debug @@ -541,6 +547,7 @@ test-suite unit-tests App.Fossa.AnalyzeSpec App.Fossa.API.BuildLinkSpec App.Fossa.API.BuildWaitSpec + App.Fossa.ApiUtilsSpec App.Fossa.ArchiveUploaderSpec App.Fossa.BinaryDeps.JarSpec App.Fossa.Config.AnalyzeSpec @@ -558,6 +565,7 @@ test-suite unit-tests App.Fossa.ManualDepsSpec App.Fossa.PathDependencySpec App.Fossa.PreflightChecksSpec + App.Fossa.Project.EditSpec App.Fossa.ProjectInferenceSpec App.Fossa.ReleaseGroup.AddProjectsSpec App.Fossa.ReleaseGroup.CommonSpec diff --git a/src/App/Fossa/ApiUtils.hs b/src/App/Fossa/ApiUtils.hs new file mode 100644 index 0000000000..7a1ba978a3 --- /dev/null +++ b/src/App/Fossa/ApiUtils.hs @@ -0,0 +1,66 @@ +module App.Fossa.ApiUtils ( + retrievePolicyId, + retrieveTeamIds, + retrieveLabelIds, + retrieveTeamIdsWithMaybe, +) where + +import Control.Algebra (Has) +import Control.Effect.Diagnostics (Diagnostics, errHelp, fatalText) +import Data.Map qualified as Map +import Data.Maybe (mapMaybe) +import Data.Text (Text, intercalate) +import Effect.Logger (pretty, renderIt, vsep) +import Fossa.API.CoreTypes (Label (..), Labels (..), Policy (..), PolicyType (..), Team (..)) + +retrievePolicyId :: Has Diagnostics sig m => Maybe Text -> PolicyType -> [Policy] -> m (Maybe Int) +retrievePolicyId maybeTitle targetType policies = case maybeTitle of + Nothing -> pure Nothing + Just targetTitle -> do + let filteredPolicies = filter (\p -> policyTitle p == targetTitle && (policyType p == targetType)) policies + case filteredPolicies of + [] -> fatalText $ "Policy `" <> targetTitle <> "` not found" + [policy] -> pure . Just $ policyId policy + (_ : _ : _) -> + errHelp ("Navigate to the FOSSA web UI to rename your policies so that they are unqiue" :: Text) + . fatalText + $ "Multiple policies with title `" <> targetTitle <> "` found. Unable to determine which policy to use." + +retrieveTeamIdsWithMaybe :: Has Diagnostics sig m => Maybe [Text] -> [Team] -> m (Maybe [Int]) +retrieveTeamIdsWithMaybe maybeTeamNames teams = case maybeTeamNames of + Nothing -> pure Nothing + Just teamNames -> Just <$> retrieveTeamIds teamNames teams + +retrieveTeamIds :: Has Diagnostics sig m => [Text] -> [Team] -> m [Int] +retrieveTeamIds teamNames teams = do + let teamMap = Map.fromList $ map (\team -> (teamName team, teamId team)) teams + validTeamIds = mapMaybe (`Map.lookup` teamMap) teamNames + + if length teamNames == length validTeamIds + then pure validTeamIds + else do + let missingTeamNames = filter (`Map.notMember` teamMap) teamNames + fatalText $ "Teams " <> intercalate "," missingTeamNames <> " not found" + +retrieveLabelIds :: Has Diagnostics sig m => [Text] -> Labels -> m ([Int], Maybe [Text]) +retrieveLabelIds projectLabels (Labels orgLabels) = do + let orgLabelMap = Map.fromList $ map (\label -> (labelName label, labelId label)) orgLabels + go orgLabelMap projectLabels [] + where + go :: Has Diagnostics sig m => Map.Map Text Int -> [Text] -> [Int] -> m ([Int], Maybe [Text]) + go _ [] acc = pure (acc, Nothing) + go labelMap (x : xs) acc = do + case Map.lookup x labelMap of + Just labelId' -> go labelMap xs (labelId' : acc) + Nothing -> do + (labelIds, maybeWarnings) <- go labelMap xs acc + let warning = + renderIt $ + vsep + [ "Label `" <> pretty x <> "` does not exist" + , "Navigate to `Organization Settings` in the FOSSA web UI to create new labels: https://app.fossa.com/account/settings/organization" + ] + let updatedWarnings = case maybeWarnings of + Just warnings -> Just (warning : warnings) + Nothing -> Just [warning] + pure (labelIds, updatedWarnings) diff --git a/src/App/Fossa/Config/Common.hs b/src/App/Fossa/Config/Common.hs index d58ce0cbbe..159b59c94b 100644 --- a/src/App/Fossa/Config/Common.hs +++ b/src/App/Fossa/Config/Common.hs @@ -10,6 +10,7 @@ module App.Fossa.Config.Common ( targetOpt, baseDirArg, metadataOpts, + parsePolicyOptions, configFileOpt, endpointOpt, apiKeyOpt, @@ -19,6 +20,7 @@ module App.Fossa.Config.Common ( validateFile, validateExists, validateApiKey, + validateApiKeyGeneric, -- * CLI Collectors collectBaseDir, @@ -43,6 +45,8 @@ module App.Fossa.Config.Common ( endpointHelp, fossaApiKeyHelp, configHelp, + titleHelp, + -- Deprecation deprecateReleaseGroupMetadata, ) where @@ -178,40 +182,41 @@ metadataOpts = <*> parsePolicyOptions <*> many (strOption (applyFossaStyle <> long "project-label" <> stringToHelpDoc "Assign up to 5 labels to the project")) <*> optional releaseGroupMetadataOpts - where - titleHelp :: Maybe (Doc AnsiStyle) - titleHelp = - Just . formatDoc $ - vsep - [ "The title of the FOSSA project" - , boldItalicized "Default: " <> "The project name" - ] - policy :: Parser Policy - policy = PolicyName <$> (strOption (applyFossaStyle <> long "policy" <> helpDoc policyHelp)) - - policyId :: Parser Policy - policyId = - PolicyId - <$> ( option - (readMWithError "failed to parse --policy-id, expecting int") - (applyFossaStyle <> long "policy-id" <> helpDoc policyIdHelp) - ) - - parsePolicyOptions :: Parser (Maybe Policy) - parsePolicyOptions = optional (policy <|> policyId) -- For Parsers '<|>' tries every alternative and fails if they all succeed. - policyHelp :: Maybe (Doc AnsiStyle) - policyHelp = - Just . formatDoc $ - vsep - [ "The name of the policy to assign to this project in FOSSA. Mutually excludes " <> coloredBoldItalicized Green "--policy-id" <> "." - ] - policyIdHelp :: Maybe (Doc AnsiStyle) - policyIdHelp = - Just . formatDoc $ - vsep - [ "The id of the policy to assign to this project in FOSSA. Mutually excludes " <> coloredBoldItalicized Green "--policy" <> "." - ] +titleHelp :: Maybe (Doc AnsiStyle) +titleHelp = + Just . formatDoc $ + vsep + [ "The title of the FOSSA project" + , boldItalicized "Default: " <> "The project name" + ] + +policy :: Parser Policy +policy = PolicyName <$> (strOption (applyFossaStyle <> long "policy" <> helpDoc policyHelp)) + +policyId :: Parser Policy +policyId = + PolicyId + <$> ( option + (readMWithError "failed to parse --policy-id, expecting int") + (applyFossaStyle <> long "policy-id" <> helpDoc policyIdHelp) + ) + +parsePolicyOptions :: Parser (Maybe Policy) +parsePolicyOptions = optional (policy <|> policyId) -- For Parsers '<|>' tries every alternative and fails if they all succeed. + +policyHelp :: Maybe (Doc AnsiStyle) +policyHelp = + Just . formatDoc $ + vsep + [ "The name of the policy to assign to this project in FOSSA. Mutually excludes " <> coloredBoldItalicized Green "--policy-id" <> "." + ] +policyIdHelp :: Maybe (Doc AnsiStyle) +policyIdHelp = + Just . formatDoc $ + vsep + [ "The id of the policy to assign to this project in FOSSA. Mutually excludes " <> coloredBoldItalicized Green "--policy" <> "." + ] releaseGroupMetadataOpts :: Parser ReleaseGroupMetadata releaseGroupMetadataOpts = @@ -329,6 +334,27 @@ validateApiKey maybeConfigFile EnvVars{envApiKey} CommonOpts{optAPIKey} = do then fatalText "A FOSSA API key was specified, but it is an empty string" else pure $ ApiKey textkey +validateApiKeyGeneric :: + ( Has Diagnostics sig m + ) => + Maybe ConfigFile -> + Maybe Text -> + Maybe Text -> + m ApiKey +validateApiKeyGeneric maybeConfigFile maybeEnvApiKey maybeOptAPIKey = do + textkey <- + fromMaybeText "A FOSSA API key is required to run this command" $ + -- API key precedence is strictly defined: + -- 1. Cmd-line option (rarely used, not encouraged) + -- 2. Config file (maybe used) + -- 3. Environment Variable (most common) + maybeOptAPIKey + <|> (maybeConfigFile >>= configApiKey) + <|> maybeEnvApiKey + if Data.Text.null . strip $ textkey + then fatalText "A FOSSA API key was specified, but it is an empty string" + else pure $ ApiKey textkey + collectApiOpts :: (Has Diagnostics sig m) => Maybe ConfigFile -> EnvVars -> CommonOpts -> m ApiOpts collectApiOpts maybeconfig envvars globals = do apikey <- validateApiKey maybeconfig envvars globals diff --git a/src/App/Fossa/Config/ConfigFile.hs b/src/App/Fossa/Config/ConfigFile.hs index 24072a5ef2..ea2f70be8b 100644 --- a/src/App/Fossa/Config/ConfigFile.hs +++ b/src/App/Fossa/Config/ConfigFile.hs @@ -210,10 +210,12 @@ data ConfigFile = ConfigFile deriving (Eq, Ord, Show) data ConfigProject = ConfigProject - { configProjID :: Maybe Text + { configProjLocator :: Maybe Text + , configProjID :: Maybe Text , configName :: Maybe Text , configLink :: Maybe Text , configTeam :: Maybe Text + , configTeams :: Maybe [Text] , configJiraKey :: Maybe Text , configUrl :: Maybe Text , configPolicy :: Maybe Policy @@ -313,10 +315,12 @@ instance FromJSON (Path Abs File -> ConfigFile) where instance FromJSON ConfigProject where parseJSON = withObject "ConfigProject" $ \obj -> ConfigProject - <$> obj .:? "id" + <$> obj .:? "locator" + <*> obj .:? "id" <*> obj .:? "name" <*> obj .:? "link" <*> obj .:? "team" + <*> obj .:? "teams" <*> obj .:? "jiraProjectKey" <*> obj .:? "url" <*> parsePolicy obj diff --git a/src/App/Fossa/Config/Project.hs b/src/App/Fossa/Config/Project.hs new file mode 100644 index 0000000000..50f95b7c91 --- /dev/null +++ b/src/App/Fossa/Config/Project.hs @@ -0,0 +1,63 @@ +module App.Fossa.Config.Project ( + ProjectCommand (..), + ProjectConfig (..), + mkSubCommand, +) where + +import App.Fossa.Config.ConfigFile (ConfigFile, resolveLocalConfigFile) +import App.Fossa.Config.EnvironmentVars (EnvVars (..)) +import App.Fossa.Config.Project.Edit as Edit + +import App.Fossa.Subcommand (EffStack, GetCommonOpts, GetSeverity (..), SubCommand (..)) +import Control.Effect.Diagnostics (Diagnostics, Has) +import Control.Effect.Lift (Lift) +import Data.Aeson (ToJSON, defaultOptions, genericToEncoding, toEncoding) +import Effect.Logger (Logger, Severity (..)) +import Effect.ReadFS (ReadFS) +import GHC.Generics (Generic) +import Options.Applicative (InfoMod, Parser, progDescDoc, subparser) +import Style (formatStringToDoc) + +projectInfo :: InfoMod a +projectInfo = progDescDoc $ formatStringToDoc "FOSSA project" + +mkSubCommand :: (ProjectConfig -> EffStack ()) -> SubCommand ProjectCommand ProjectConfig +mkSubCommand = SubCommand "project" projectInfo projectCliParser loadConfig projectMergeOpts + +newtype ProjectCommand = Edit EditOpts + +newtype ProjectConfig = EditCfg EditConfig + deriving (Show, Generic) + +instance GetCommonOpts ProjectCommand + +instance ToJSON ProjectConfig where + toEncoding = genericToEncoding defaultOptions + +instance GetSeverity ProjectCommand where + getSeverity :: ProjectCommand -> Severity + getSeverity (Edit (EditOpts{debug})) = if debug then SevDebug else SevInfo + +projectMergeOpts :: + ( Has Diagnostics sig m + ) => + Maybe ConfigFile -> + EnvVars -> + ProjectCommand -> + m ProjectConfig +projectMergeOpts cfg envvars (Edit opts) = EditCfg <$> Edit.mergeOpts cfg envvars opts + +loadConfig :: + ( Has Diagnostics sig m + , Has Logger sig m + , Has ReadFS sig m + , Has (Lift IO) sig m + ) => + ProjectCommand -> + m (Maybe ConfigFile) +loadConfig (Edit opts) = resolveLocalConfigFile $ Edit.configOpts opts + +projectCliParser :: Parser ProjectCommand +projectCliParser = + subparser $ + Edit.subcommand Edit diff --git a/src/App/Fossa/Config/Project/Edit.hs b/src/App/Fossa/Config/Project/Edit.hs new file mode 100644 index 0000000000..447cedeb93 --- /dev/null +++ b/src/App/Fossa/Config/Project/Edit.hs @@ -0,0 +1,198 @@ +{-# LANGUAGE RecordWildCards #-} + +module App.Fossa.Config.Project.Edit ( + EditConfig (..), + EditOpts (..), + ProjectIdentifier (..), + subcommand, + cliParser, + mergeOpts, +) where + +import App.Fossa.Config.Common (configHelp, endpointHelp, fossaApiKeyCmdText, fossaApiKeyHelp, parsePolicyOptions, titleHelp, validateApiKeyGeneric) +import App.Fossa.Config.ConfigFile (ConfigFile (..), ConfigProject (..)) +import App.Fossa.Config.EnvironmentVars (EnvVars (envApiKey)) +import App.OptionExtensions (uriOption) +import App.Types (Policy (..)) +import Control.Effect.Diagnostics (Diagnostics, Has, fatalText) +import Data.Aeson (ToJSON, defaultOptions, genericToEncoding, toEncoding) +import Data.Text (Text) +import Data.Text qualified +import Effect.Logger (vsep) +import Fossa.API.Types (ApiOpts (..), defaultApiPollDelay) +import GHC.Generics (Generic) +import Options.Applicative ( + CommandFields, + InfoMod, + Mod, + Parser, + command, + helpDoc, + info, + long, + metavar, + optional, + short, + some, + strOption, + switch, + (<|>), + ) +import Options.Applicative.Builder (progDescDoc) +import Options.Applicative.Help (AnsiStyle) +import Prettyprinter (Doc) +import Style (applyFossaStyle, boldItalicized, formatDoc, formatStringToDoc, stringToHelpDoc) +import Text.URI (URI, mkURI) + +projectEditInfo :: InfoMod a +projectEditInfo = progDescDoc $ formatStringToDoc "Edit a FOSSA project" + +subcommand :: (EditOpts -> a) -> Mod CommandFields a +subcommand f = command "edit" $ info (f <$> cliParser) projectEditInfo + +data EditConfig = EditConfig + { apiOpts :: ApiOpts + , projectIdentifier :: ProjectIdentifier + , projectTitle :: Maybe Text + , projectUrl :: Maybe Text + , projectJiraKey :: Maybe Text + , projectPolicy :: Maybe Policy + , projectLink :: Maybe Text + , projectLabels :: Maybe [Text] + , -- Teams to add project + teams :: Maybe [Text] + } + deriving (Eq, Ord, Show, Generic) + +instance ToJSON EditConfig where + toEncoding = genericToEncoding defaultOptions + +data ProjectIdentifier + = ProjectId Text + | ProjectLocator Text + deriving (Eq, Ord, Show, Generic) + +instance ToJSON ProjectIdentifier where + toEncoding = genericToEncoding defaultOptions + +data EditOpts = EditOpts + { debug :: Bool + , baseUrlOpts :: Maybe URI + , apiKeyOpts :: Maybe Text + , configOpts :: Maybe FilePath + , projectIdentiferOpts :: Maybe ProjectIdentifier + , projectTitleOpts :: Maybe Text + , projectUrlOpts :: Maybe Text + , projectJiraKeyOpts :: Maybe Text + , projectPolicyOpts :: Maybe Policy + , projectLinkOpts :: Maybe Text + , projectLabelsOpts :: Maybe [Text] + , teamsOpts :: Maybe [Text] + } + deriving (Eq, Ord, Show, Generic) + +cliParser :: Parser EditOpts +cliParser = + EditOpts + <$> switch (applyFossaStyle <> long "debug" <> stringToHelpDoc "Enable debug logging") + <*> optional (uriOption (applyFossaStyle <> long "endpoint" <> short 'e' <> metavar "URL" <> helpDoc endpointHelp)) + <*> optional (strOption (applyFossaStyle <> long fossaApiKeyCmdText <> helpDoc fossaApiKeyHelp)) + <*> optional (strOption (applyFossaStyle <> long "config" <> short 'c' <> helpDoc configHelp)) + <*> projectIdentiferOptions + <*> optional (strOption (applyFossaStyle <> long "title" <> short 't' <> helpDoc titleHelp)) + <*> optional (strOption (applyFossaStyle <> long "project-url" <> short 'P' <> stringToHelpDoc "The url of the project's repository.")) + <*> optional (strOption (applyFossaStyle <> long "jira-project-key" <> short 'j' <> stringToHelpDoc "The JIRA project key to associate with the project.")) + <*> parsePolicyOptions + <*> optional (strOption (applyFossaStyle <> long "link" <> short 'L' <> stringToHelpDoc "A link to attach to the project.")) + <*> optional (some (strOption (applyFossaStyle <> long "project-label" <> stringToHelpDoc "Assign up to 5 labels to the project."))) + <*> optional (some (strOption (applyFossaStyle <> long "team" <> short 'T' <> stringToHelpDoc "The teams to associate with the project."))) + +projectIdHelp :: Maybe (Doc AnsiStyle) +projectIdHelp = + Just . formatDoc $ + vsep + [ "The project ID defines an ID that is used to reference a project within your FOSSA organization." + , "" + , "If the project ID was not explicitly set during the time of project creation, it defaults to:" + , boldItalicized "Git: " <> "The .git/config file or project's remote `origin` URL" + , boldItalicized "SVN: " <> "The `Repository Root` obtained using 'svn info'" + , boldItalicized "No VCS: " <> "The name of the project's directory" + ] + +projectIdOptions :: Parser ProjectIdentifier +projectIdOptions = ProjectId <$> (strOption (applyFossaStyle <> long "project-id" <> helpDoc projectIdHelp)) + +projectLocatorOptions :: Parser ProjectIdentifier +projectLocatorOptions = ProjectLocator <$> (strOption (applyFossaStyle <> long "project-locator" <> stringToHelpDoc "The project locator defines a unique ID that the FOSSA API will use to reference this project within FOSSA.")) + +projectIdentiferOptions :: Parser (Maybe ProjectIdentifier) +projectIdentiferOptions = optional (projectIdOptions <|> projectLocatorOptions) + +mergeOpts :: + ( Has Diagnostics sig m + ) => + Maybe ConfigFile -> + EnvVars -> + EditOpts -> + m EditConfig +mergeOpts maybeConfig envVars cliOpts@EditOpts{..} = do + apiOpts <- collectApiOpts maybeConfig envVars cliOpts + maybePolicy <- maybe (pure projectPolicyOpts) (mergePolicy projectPolicyOpts) maybeConfig + let maybeProjectIdentifer = maybe projectIdentiferOpts (mergeProjectIdentifier projectIdentiferOpts) maybeConfig + + case maybeProjectIdentifer of + Just projId@(ProjectId projectId') + | not (Data.Text.null projectId') -> + pure $ mkConfig apiOpts projId maybePolicy + Just projId@(ProjectLocator locatorVal) + | not (Data.Text.null locatorVal) -> + pure $ mkConfig apiOpts projId maybePolicy + _ -> fatalText "Either projectId or project locator needs to be specified to edit a FOSSA project" + where + mkConfig :: ApiOpts -> ProjectIdentifier -> Maybe Policy -> EditConfig + mkConfig apiOpts projectIdentifer maybePolicy = do + let maybeProjectConfig = maybeConfig >>= configProject + configProjectLabels = maybe [] configLabel maybeProjectConfig + EditConfig + { apiOpts = apiOpts + , projectIdentifier = projectIdentifer + , projectTitle = projectTitleOpts <|> extractProjectConfigVal maybeConfig configName + , projectUrl = projectUrlOpts <|> extractProjectConfigVal maybeConfig configUrl + , projectJiraKey = projectJiraKeyOpts <|> extractProjectConfigVal maybeConfig configJiraKey + , projectPolicy = maybePolicy + , projectLink = projectLinkOpts <|> extractProjectConfigVal maybeConfig configLink + , projectLabels = projectLabelsOpts <|> labelConfigToMaybe configProjectLabels + , -- Teams to add project to. + -- Precdence for teams is CLI input, `project.teams` field from config, then `project.team` field from config. + -- Account for `project.team` for backwards compatability. + teams = teamsOpts <|> extractProjectConfigVal maybeConfig configTeams <|> ((: []) <$> extractProjectConfigVal maybeConfig configTeam) + } + + labelConfigToMaybe :: [Text] -> Maybe [Text] + labelConfigToMaybe [] = Nothing + labelConfigToMaybe xs = Just xs + +collectApiOpts :: (Has Diagnostics sig m) => Maybe ConfigFile -> EnvVars -> EditOpts -> m ApiOpts +collectApiOpts maybeConfig envVars EditOpts{..} = do + apikey <- validateApiKeyGeneric maybeConfig (envApiKey envVars) apiKeyOpts + let configUri = maybeConfig >>= configServer >>= mkURI + baseuri = baseUrlOpts <|> configUri + pure $ ApiOpts baseuri apikey defaultApiPollDelay + +extractProjectConfigVal :: Maybe ConfigFile -> (ConfigProject -> Maybe a) -> Maybe a +extractProjectConfigVal maybeConfig getValue = do + let projectCfg = maybeConfig >>= configProject + projectCfg >>= getValue + +mergeProjectIdentifier :: Maybe ProjectIdentifier -> ConfigFile -> Maybe ProjectIdentifier +mergeProjectIdentifier maybeProjectIdentifer config = maybeProjectIdentifer <|> ProjectId <$> (configProject config >>= configProjID) <|> ProjectLocator <$> (configProject config >>= configProjLocator) + +mergePolicy :: Has Diagnostics sig m => Maybe Policy -> ConfigFile -> m (Maybe Policy) +mergePolicy maybePolicy config = + case (maybePolicy, cfgPolicy) of + (Just (PolicyId _), Just (PolicyName _)) -> err + (Just (PolicyName _), Just (PolicyId _)) -> err + _ -> pure $ maybePolicy <|> cfgPolicy + where + err = fatalText "Only one of policy or policyId can be set. Check your cli options and .fossa.yml to ensure you aren't specifying both." + cfgPolicy = configProject config >>= configPolicy diff --git a/src/App/Fossa/Init/.fossa.yml b/src/App/Fossa/Init/.fossa.yml index 038447f697..ab4cc0e8b1 100644 --- a/src/App/Fossa/Init/.fossa.yml +++ b/src/App/Fossa/Init/.fossa.yml @@ -44,14 +44,20 @@ version: 3 # # # # NOTE: # # Can only be set when creating a project (running fossa analyze for the first time), -# # Otherwise, they will be silently ignored. +# # Otherwise, they will be silently ignored. Use fossa project edit to modify this field for existing projects. # name: fossa-cli # # # Name of the team in your FOSSA organization to associate with this project. # # NOTE: # # Can only be set when creating a project (running fossa analyze for the first time), -# # Otherwise, they will be silently ignored. -# team: cli-team +# # Otherwise, they will be silently ignored. Use fossa project edit to modify this field for existing projects. +# team: example-team +# +# # Name of the teams in your FOSSA organization to associate with this project. +# # Currently, this field is only supported through fossa project edit. +# teams: +# - example-team-1 +# - example-team-2 # # # Name of the policy in your FOSSA organization to associate with this project. # # NOTE: @@ -62,7 +68,7 @@ version: 3 # # An external link that will appear in the FOSSA UI for this specific project. # # NOTE: # # Can only be set when creating a project (running fossa analyze for the first time), -# # Otherwise, they will be silently ignored. +# # Otherwise, they will be silently ignored. Use fossa project edit to modify this field for existing projects. # link: fossa.com # # # The URL of your project that will appear in FOSSA. @@ -74,7 +80,7 @@ version: 3 # # # # NOTE: # # Can only be set when creating a project (running fossa analyze for the first time), -# # Otherwise, they will be silently ignored. +# # Otherwise, they will be silently ignored. Use fossa project edit to modify this field for existing projects. # jiraProjectKey: jira-key # # # The 'name' and 'release' of the release group's release to add your project to in the FOSSA dashboard. diff --git a/src/App/Fossa/Main.hs b/src/App/Fossa/Main.hs index e8fcf3bc42..4bef13c722 100644 --- a/src/App/Fossa/Main.hs +++ b/src/App/Fossa/Main.hs @@ -8,6 +8,7 @@ import App.Fossa.DumpBinaries qualified as Dump import App.Fossa.Init (initCommand) import App.Fossa.LicenseScan qualified as LicenseScan (licenseScanSubCommand) import App.Fossa.ListTargets qualified as ListTargets +import App.Fossa.Project qualified as Project import App.Fossa.ReleaseGroup qualified as ReleaseGroup import App.Fossa.Report qualified as Report import App.Fossa.Snippets qualified as Snippets @@ -103,6 +104,7 @@ subcommands = public <|> private , decodeSubCommand Snippets.snippetsSubCommand , initCommand , feedbackCommand + , decodeSubCommand Project.projectSubCommand , decodeSubCommand ReleaseGroup.releaseGroupSubCommand ] diff --git a/src/App/Fossa/Project.hs b/src/App/Fossa/Project.hs new file mode 100644 index 0000000000..37cd25910a --- /dev/null +++ b/src/App/Fossa/Project.hs @@ -0,0 +1,30 @@ +module App.Fossa.Project ( + projectSubCommand, +) where + +import App.Fossa.Config.Project (ProjectCommand (..), ProjectConfig (..), mkSubCommand) +import App.Fossa.Config.Project.Edit qualified as Edit +import App.Fossa.Project.Edit (editMain) +import App.Fossa.Subcommand (SubCommand) +import Control.Algebra (Has) +import Control.Carrier.Debug (ignoreDebug) +import Control.Carrier.Diagnostics (context) +import Control.Carrier.FossaApiClient (runFossaApiClient) +import Control.Carrier.StickyLogger (runStickyLogger) +import Control.Effect.Diagnostics (Diagnostics) +import Control.Effect.Lift (Lift) +import Effect.Logger (Logger, Severity (SevInfo), logInfo) + +projectSubCommand :: SubCommand ProjectCommand ProjectConfig +projectSubCommand = mkSubCommand projectMain + +projectMain :: + ( Has (Lift IO) sig m + , Has Diagnostics sig m + , Has Logger sig m + ) => + ProjectConfig -> + m () +projectMain (EditCfg config) = do + logInfo "Running FOSSA project" + context "Add projects to release group" . runStickyLogger SevInfo . ignoreDebug . runFossaApiClient (Edit.apiOpts config) $ editMain config diff --git a/src/App/Fossa/Project/Edit.hs b/src/App/Fossa/Project/Edit.hs new file mode 100644 index 0000000000..070955c483 --- /dev/null +++ b/src/App/Fossa/Project/Edit.hs @@ -0,0 +1,170 @@ +{-# LANGUAGE RecordWildCards #-} + +module App.Fossa.Project.Edit ( + editMain, +) where + +import App.Fossa.ApiUtils (retrieveLabelIds, retrievePolicyId, retrieveTeamIds) +import App.Fossa.Config.Project.Edit (EditConfig (..), ProjectIdentifier (..)) +import App.Types (Policy (..)) +import Control.Algebra (Has) +import Control.Carrier.Diagnostics (context, errHelp) +import Control.Carrier.StickyLogger (logSticky) +import Control.Effect.Diagnostics (Diagnostics, fatalText, warn) +import Control.Effect.FossaApiClient (FossaApiClient, addTeamProjects, getOrgLabels, getOrganization, getPolicies, getProjectV2, getTeams, updateProject, updateRevision) +import Control.Effect.Lift (Lift) +import Control.Effect.StickyLogger (StickyLogger) +import Control.Monad (void) +import Data.Foldable (traverse_) +import Data.Maybe (isJust) +import Data.String.Conversion (toText) +import Data.Text (Text, intercalate) +import Effect.Logger (Logger, logDebug, logInfo, logStdout, pretty) +import Fossa.API.CoreTypes (AddTeamProjectsRequest (..), AddTeamProjectsResponse (teamProjectLocators), PolicyType (LICENSING), Project (..), TeamProjectAction (Add), UpdateProjectRequest (..), UpdateRevisionRequest (UpdateRevisionRequest)) +import Fossa.API.Types (Organization (organizationId)) +import Text.Pretty.Simple (pShow) + +editMain :: + ( Has Diagnostics sig m + , Has Logger sig m + , Has (Lift IO) sig m + , Has FossaApiClient sig m + , Has StickyLogger sig m + ) => + EditConfig -> + m () +editMain EditConfig{..} = do + logInfo "Running FOSSA project edit" + + projectLocator <- case projectIdentifier of + ProjectLocator projectLocator -> pure projectLocator + ProjectId projectId -> do + org <- getOrganization + pure $ constructProjectLocatorFromProjectId org projectId + + project <- getProjectV2 projectLocator + + maybeWarnings <- attemptToUpdateProjectMetadata projectLocator (projectDefaultBranch project) $ projectIssueTrackerIds project + attemptToAddProjectToTeams projectLocator teams + attemptToUpdateRevisionMetadata projectLocator (projectLatestRevision project) + + case maybeWarnings of + Nothing -> logStdout $ "Project " <> "`" <> projectLocator <> "` has been updated successfully." <> "\n" + Just projectUpdateWarnings -> do + logStdout $ "Project " <> "`" <> projectLocator <> "` has been updated with warnings. Run the command with `--debug` to view the warnings." <> "\n" + traverse_ warn projectUpdateWarnings + where + constructProjectLocatorFromProjectId :: Organization -> Text -> Text + constructProjectLocatorFromProjectId org projId = "custom+" <> toText (show $ organizationId org) <> "/" <> projId + + attemptToUpdateProjectMetadata :: + ( Has FossaApiClient sig m + , Has Diagnostics sig m + , Has StickyLogger sig m + , Has Logger sig m + ) => + Text -> + Maybe Text -> + Maybe [Text] -> + m (Maybe [Text]) + attemptToUpdateProjectMetadata projectLocator maybeDefaultBranch currentIssueTrackerIds = do + -- Ensure that at least one project metadata field is being updated, otherwise the endpoint will throw an error + if (or [isJust projectTitle, isJust projectUrl, isJust projectJiraKey, isJust projectLabels, isJust projectPolicy]) + then do + logSticky "Updating project metadata" + maybePolicyId <- case projectPolicy of + Nothing -> pure Nothing + Just policy -> case policy of + PolicyId policyId -> pure $ Just policyId + PolicyName policyName -> do + policies <- getPolicies + context "Retrieving license policy ID" $ retrievePolicyId (Just policyName) LICENSING policies + + orgLabels <- getOrgLabels + (maybeLabelIds, maybeLabelWarnings) <- case projectLabels of + Nothing -> pure (Nothing, Nothing) + Just labels -> do + (labelIds, maybeLabelWarnings) <- context "Retrieving label Ids" $ retrieveLabelIds (labels) orgLabels + pure (Just labelIds, maybeLabelWarnings) + + -- Updating the project's issue tracker ids is an all or nothing transaction. + -- Add the specified jira key to the current list of issue tracker ids if it isn't already present. + -- If it is present in the list, use the project's existing issue tracker ids to ensure nothing is overwritten. + let udpatedIssueTrackerIds = case projectJiraKey of + Nothing -> Nothing + Just jiraKey -> Just . updateIssueTrackerIds jiraKey =<< currentIssueTrackerIds + let req = + UpdateProjectRequest + { updateProjectTitle = projectTitle + , updateProjectUrl = projectUrl + , updateProjectIssueTrackerIds = udpatedIssueTrackerIds + , updateProjectLabelIds = maybeLabelIds + , updateProjectPolicyId = maybePolicyId + , -- This field needs to be set, otherwise the default branch will be removed + updateProjectDefaultBranch = maybeDefaultBranch + } + res <- updateProject projectLocator req + logDebug $ "Updated project: " <> pretty (pShow res) + pure maybeLabelWarnings + else pure Nothing + + updateIssueTrackerIds :: Text -> [Text] -> [Text] + updateIssueTrackerIds jiraKey currentIssueTrackerIds = + if jiraKey `elem` currentIssueTrackerIds + then currentIssueTrackerIds + else currentIssueTrackerIds ++ [jiraKey] + + attemptToUpdateRevisionMetadata :: + ( Has FossaApiClient sig m + , Has Diagnostics sig m + , Has StickyLogger sig m + ) => + Text -> + Maybe Text -> + m () + attemptToUpdateRevisionMetadata projectLocator maybeRevisionLocator = do + case projectLink of + Nothing -> pure () + Just projectLink' -> case maybeRevisionLocator of + Nothing -> + errHelp ("Ensure your project has completed a scan in order to update the revision link" :: Text) $ + fatalText $ + "No revision found for project: `" <> projectLocator <> "`" + Just revisionLocator -> do + logSticky "Updating revision metadata" + void $ updateRevision revisionLocator $ UpdateRevisionRequest projectLink' + + attemptToAddProjectToTeams :: + ( Has FossaApiClient sig m + , Has Diagnostics sig m + , Has Logger sig m + , Has StickyLogger sig m + ) => + Text -> + Maybe [Text] -> + m () + attemptToAddProjectToTeams projectLocator maybeTeamNames = do + case maybeTeamNames of + Nothing -> pure () + Just teamNames -> do + logSticky $ "Adding Project: `" <> projectLocator <> "` to Teams: `" <> intercalate ", " teamNames <> "`" + orgTeams <- getTeams + teamIds <- context "Retrieving team IDs" $ retrieveTeamIds teamNames orgTeams + + logSticky "Teams in org:" + logDebug $ pretty (pShow orgTeams) + traverse_ (addTeamProjectWithLogs projectLocator) teamIds + + addTeamProjectWithLogs :: + ( Has FossaApiClient sig m + , Has Logger sig m + , Has StickyLogger sig m + ) => + Text -> + Int -> + m () + addTeamProjectWithLogs projectLocator teamId = do + res <- addTeamProjects teamId $ AddTeamProjectsRequest [projectLocator] Add + logSticky $ "Project: `" <> projectLocator <> "` added to teamId: `" <> toText teamId <> "`" + logSticky $ "All projects in teamID: `" <> toText teamId <> "`" + logDebug $ pretty (pShow (teamProjectLocators res)) diff --git a/src/App/Fossa/ReleaseGroup/AddProjects.hs b/src/App/Fossa/ReleaseGroup/AddProjects.hs index d6b23302d2..4de3043d90 100644 --- a/src/App/Fossa/ReleaseGroup/AddProjects.hs +++ b/src/App/Fossa/ReleaseGroup/AddProjects.hs @@ -16,7 +16,7 @@ import Data.Set qualified as Set import Data.String.Conversion (ToText (..)) import Data.Text (Text) import Effect.Logger (Logger, logDebug, logInfo, logStdout, pretty) -import Fossa.API.Types (ReleaseGroupRelease (..), ReleaseProject (..), UpdateReleaseProjectRequest (..), UpdateReleaseRequest (..)) +import Fossa.API.CoreTypes (ReleaseGroupRelease (..), ReleaseProject (..), UpdateReleaseProjectRequest (..), UpdateReleaseRequest (..)) import Text.Pretty.Simple (pShow) addProjectsMain :: diff --git a/src/App/Fossa/ReleaseGroup/Common.hs b/src/App/Fossa/ReleaseGroup/Common.hs index 6dec089130..b7f629d68b 100644 --- a/src/App/Fossa/ReleaseGroup/Common.hs +++ b/src/App/Fossa/ReleaseGroup/Common.hs @@ -6,7 +6,7 @@ module App.Fossa.ReleaseGroup.Common ( import Control.Algebra (Has) import Control.Effect.Diagnostics (Diagnostics, errHelp, fatalText) import Data.Text (Text) -import Fossa.API.Types (ReleaseGroup (..), ReleaseGroupRelease (..)) +import Fossa.API.CoreTypes (ReleaseGroup (..), ReleaseGroupRelease (..)) retrieveReleaseGroupId :: Has Diagnostics sig m => Text -> [ReleaseGroup] -> m (Maybe Int) retrieveReleaseGroupId title releaseGroups = do diff --git a/src/App/Fossa/ReleaseGroup/Create.hs b/src/App/Fossa/ReleaseGroup/Create.hs index 0eeb29288e..9866b7cb69 100644 --- a/src/App/Fossa/ReleaseGroup/Create.hs +++ b/src/App/Fossa/ReleaseGroup/Create.hs @@ -2,10 +2,9 @@ module App.Fossa.ReleaseGroup.Create ( createMain, - retrievePolicyId, - retrieveTeamIds, ) where +import App.Fossa.ApiUtils (retrievePolicyId, retrieveTeamIdsWithMaybe) import App.Fossa.Config.ReleaseGroup.Create (CreateConfig (..)) import App.Fossa.ReleaseGroup.Common (retrieveReleaseGroupId) import App.Types (ReleaseGroupRevision (..)) @@ -14,12 +13,11 @@ import Control.Effect.Diagnostics (Diagnostics, context, errHelp, fatalText) import Control.Effect.FossaApiClient (FossaApiClient, createReleaseGroup, getPolicies, getReleaseGroups, getTeams) import Control.Effect.Lift (Lift) import Control.Monad (when) -import Data.Map qualified as Map -import Data.Maybe (isJust, mapMaybe) +import Data.Maybe (isJust) import Data.String.Conversion (ToText (..)) -import Data.Text (Text, intercalate) +import Data.Text (Text) import Effect.Logger (Logger, logInfo, logStdout) -import Fossa.API.Types (CreateReleaseGroupRequest (..), CreateReleaseGroupResponse (..), Policy (..), PolicyType (..), Team (..)) +import Fossa.API.CoreTypes (CreateReleaseGroupRequest (..), CreateReleaseGroupResponse (..), PolicyType (..)) createMain :: ( Has Diagnostics sig m @@ -47,7 +45,7 @@ createMain CreateConfig{..} = do maybeLicensePolicyId <- context "Retrieving license policy ID" $ retrievePolicyId (releaseGroupLicensePolicy releaseGroupRevision) LICENSING policies maybeSecurityPolicyId <- context "Retrieving security policy ID" $ retrievePolicyId (releaseGroupSecurityPolicy releaseGroupRevision) SECURITY policies maybeQualityPolicyId <- context "Retrieving quality policy ID" $ retrievePolicyId (releaseGroupQualityPolicy releaseGroupRevision) QUALITY policies - maybeTeamIds <- context "Retrieving team IDs" $ retrieveTeamIds (releaseGroupTeams releaseGroupRevision) teams + maybeTeamIds <- context "Retrieving team IDs" $ retrieveTeamIdsWithMaybe (releaseGroupTeams releaseGroupRevision) teams let req = CreateReleaseGroupRequest (releaseGroupTitle releaseGroupRevision) (releaseGroupReleaseRevision releaseGroupRevision) maybeLicensePolicyId maybeSecurityPolicyId maybeQualityPolicyId maybeTeamIds res <- createReleaseGroup req @@ -56,29 +54,3 @@ createMain CreateConfig{..} = do where createdReleaseGroupLink :: Int -> Text createdReleaseGroupLink releaseGroupId = "https://app.fossa.com/projects/group/" <> toText releaseGroupId - -retrievePolicyId :: Has Diagnostics sig m => Maybe Text -> PolicyType -> [Policy] -> m (Maybe Int) -retrievePolicyId maybeTitle targetType policies = case maybeTitle of - Nothing -> pure Nothing - Just targetTitle -> do - let filteredPolicies = filter (\p -> policyTitle p == targetTitle && (policyType p == targetType)) policies - case filteredPolicies of - [] -> fatalText $ "Policy `" <> targetTitle <> "` not found" - [policy] -> pure . Just $ policyId policy - (_ : _ : _) -> - errHelp ("Navigate to the FOSSA web UI to rename your policies so that they are unqiue" :: Text) - . fatalText - $ "Multiple policies with title `" <> targetTitle <> "` found. Unable to determine which policy to use." - -retrieveTeamIds :: Has Diagnostics sig m => Maybe [Text] -> [Team] -> m (Maybe [Int]) -retrieveTeamIds maybeTeamNames teams = case maybeTeamNames of - Nothing -> pure Nothing - Just teamNames -> do - let teamMap = Map.fromList $ map (\team -> (teamName team, teamId team)) teams - validTeamIds = mapMaybe (`Map.lookup` teamMap) teamNames - - if length teamNames == length validTeamIds - then pure . Just $ validTeamIds - else do - let missingTeamNames = filter (`Map.notMember` teamMap) teamNames - fatalText $ "Teams " <> intercalate "," missingTeamNames <> "not found" diff --git a/src/App/Fossa/ReleaseGroup/CreateRelease.hs b/src/App/Fossa/ReleaseGroup/CreateRelease.hs index 380059f2e6..441753ca0d 100644 --- a/src/App/Fossa/ReleaseGroup/CreateRelease.hs +++ b/src/App/Fossa/ReleaseGroup/CreateRelease.hs @@ -13,7 +13,7 @@ import Control.Effect.Lift (Lift) import Data.String.Conversion (ToText (..)) import Data.Text (Text) import Effect.Logger (Logger, logInfo, logStdout) -import Fossa.API.Types (ReleaseGroupRelease (..)) +import Fossa.API.CoreTypes (ReleaseGroupRelease (..)) createReleaseMain :: ( Has Diagnostics sig m diff --git a/src/App/Fossa/ReleaseGroup/DeleteRelease.hs b/src/App/Fossa/ReleaseGroup/DeleteRelease.hs index 96fb87e007..e002332e52 100644 --- a/src/App/Fossa/ReleaseGroup/DeleteRelease.hs +++ b/src/App/Fossa/ReleaseGroup/DeleteRelease.hs @@ -12,7 +12,7 @@ import Control.Effect.FossaApiClient (FossaApiClient, deleteReleaseGroupRelease, import Control.Effect.Lift (Lift) import Control.Monad (when) import Effect.Logger (Logger, logInfo, logStdout) -import Fossa.API.Types (ReleaseGroupRelease (..)) +import Fossa.API.CoreTypes (ReleaseGroupRelease (..)) deleteReleaseMain :: ( Has Diagnostics sig m diff --git a/src/Control/Carrier/FossaApiClient.hs b/src/Control/Carrier/FossaApiClient.hs index ce10440e76..efc42d9e0c 100644 --- a/src/Control/Carrier/FossaApiClient.hs +++ b/src/Control/Carrier/FossaApiClient.hs @@ -47,6 +47,7 @@ runFossaApiClient apiOpts = GetPolicies -> Core.getPolicies GetProject rev -> Core.getProject rev GetTeams -> Core.getTeams + AddTeamProjects teamId req -> Core.addTeamProjects teamId req GetAnalyzedRevisions vdeps -> Core.getAnalyzedRevisions vdeps GetSignedFirstPartyScanUrl rev -> LicenseScanning.getSignedFirstPartyScanUrl rev GetSignedLicenseScanUrl rev -> LicenseScanning.getSignedLicenseScanUrl rev @@ -77,5 +78,12 @@ runFossaApiClient apiOpts = GetReleaseGroups -> Core.getReleaseGroups GetReleaseGroupReleases releaseGroupId -> Core.getReleaseGroupReleases releaseGroupId CreateReleaseGroup req -> Core.createReleaseGroup req + -- Project + GetProjectV2 locator -> Core.getProjectV2 locator + UpdateProject locator req -> Core.updateProject locator req + -- Revision + UpdateRevision revisionLocator req -> Core.updateRevision revisionLocator req + -- Labels + GetOrgLabels -> Core.getOrgLabels CreateReleaseGroupRelease releaseGroupId req -> Core.createReleaseGroupRelease releaseGroupId req ) diff --git a/src/Control/Carrier/FossaApiClient/Internal/Core.hs b/src/Control/Carrier/FossaApiClient/Internal/Core.hs index 0fc7505533..8a35f96f95 100644 --- a/src/Control/Carrier/FossaApiClient/Internal/Core.hs +++ b/src/Control/Carrier/FossaApiClient/Internal/Core.hs @@ -29,6 +29,11 @@ module Control.Carrier.FossaApiClient.Internal.Core ( getReleaseGroups, getReleaseGroupReleases, updateReleaseGroupRelease, + getProjectV2, + updateProject, + addTeamProjects, + updateRevision, + getOrgLabels, ) where import App.Fossa.Config.Report (ReportOutputFormat) @@ -54,22 +59,18 @@ import Fossa.API.Types ( Archive, Build, Contributors, - CreateReleaseGroupRequest, - CreateReleaseGroupResponse, CustomBuildUploadPermissions, Issues, Organization, - Policy, Project, - ReleaseGroup, - ReleaseGroupRelease, RevisionDependencyCache, SignedURL, - Team, TokenTypeResponse, - UpdateReleaseRequest, UploadResponse, ) + +import Fossa.API.CoreTypes qualified as CoreTypes + import Srclib.Types (Locator, SourceUnit, renderLocator) -- Fetches an organization from the API @@ -319,8 +320,8 @@ createReleaseGroup :: , Has Debug sig m , Has (Reader ApiOpts) sig m ) => - CreateReleaseGroupRequest -> - m CreateReleaseGroupResponse + CoreTypes.CreateReleaseGroupRequest -> + m CoreTypes.CreateReleaseGroupResponse createReleaseGroup req = do apiOpts <- ask API.createReleaseGroup apiOpts req @@ -333,7 +334,7 @@ createReleaseGroupRelease :: ) => Int -> ReleaseGroupReleaseRevision -> - m ReleaseGroupRelease + m CoreTypes.ReleaseGroupRelease createReleaseGroupRelease releaseGroupId req = do apiOpts <- ask API.createReleaseGroupRelease apiOpts releaseGroupId req @@ -344,7 +345,7 @@ getPolicies :: , Has Debug sig m , Has (Reader ApiOpts) sig m ) => - m [Policy] + m [CoreTypes.Policy] getPolicies = do apiOpts <- ask API.getPolicies apiOpts @@ -355,11 +356,24 @@ getTeams :: , Has Debug sig m , Has (Reader ApiOpts) sig m ) => - m [Team] + m [CoreTypes.Team] getTeams = do apiOpts <- ask API.getTeams apiOpts +addTeamProjects :: + ( Has (Lift IO) sig m + , Has Diagnostics sig m + , Has Debug sig m + , Has (Reader ApiOpts) sig m + ) => + Int -> + CoreTypes.AddTeamProjectsRequest -> + m CoreTypes.AddTeamProjectsResponse +addTeamProjects teamId req = do + apiOpts <- ask + API.addTeamProjects apiOpts teamId req + deleteReleaseGroup :: ( Has (Lift IO) sig m , Has Diagnostics sig m @@ -393,8 +407,8 @@ updateReleaseGroupRelease :: ) => Int -> Int -> - UpdateReleaseRequest -> - m ReleaseGroupRelease + CoreTypes.UpdateReleaseRequest -> + m CoreTypes.ReleaseGroupRelease updateReleaseGroupRelease releaseGroupId releaseId updateReq = do apiOpts <- ask API.updateReleaseGroupRelease apiOpts releaseGroupId releaseId updateReq @@ -405,7 +419,7 @@ getReleaseGroups :: , Has Debug sig m , Has (Reader ApiOpts) sig m ) => - m [ReleaseGroup] + m [CoreTypes.ReleaseGroup] getReleaseGroups = do apiOpts <- ask API.getReleaseGroups apiOpts @@ -417,7 +431,56 @@ getReleaseGroupReleases :: , Has (Reader ApiOpts) sig m ) => Int -> - m [ReleaseGroupRelease] + m [CoreTypes.ReleaseGroupRelease] getReleaseGroupReleases releaseGroupId = do apiOpts <- ask API.getReleaseGroupReleases apiOpts releaseGroupId + +getProjectV2 :: + ( Has (Lift IO) sig m + , Has Diagnostics sig m + , Has Debug sig m + , Has (Reader ApiOpts) sig m + ) => + Text -> + m CoreTypes.Project +getProjectV2 locator = do + apiOpts <- ask + API.getProjectV2 apiOpts locator + +updateProject :: + ( Has (Lift IO) sig m + , Has Diagnostics sig m + , Has Debug sig m + , Has (Reader ApiOpts) sig m + ) => + Text -> + CoreTypes.UpdateProjectRequest -> + m CoreTypes.Project +updateProject locator req = do + apiOpts <- ask + API.updateProject apiOpts locator req + +updateRevision :: + ( Has (Lift IO) sig m + , Has Diagnostics sig m + , Has Debug sig m + , Has (Reader ApiOpts) sig m + ) => + Text -> + CoreTypes.UpdateRevisionRequest -> + m CoreTypes.Revision +updateRevision revisionLocator req = do + apiOpts <- ask + API.updateRevision apiOpts revisionLocator req + +getOrgLabels :: + ( Has (Lift IO) sig m + , Has Diagnostics sig m + , Has Debug sig m + , Has (Reader ApiOpts) sig m + ) => + m CoreTypes.Labels +getOrgLabels = do + apiOpts <- ask + API.getOrgLabels apiOpts diff --git a/src/Control/Carrier/FossaApiClient/Internal/FossaAPIV1.hs b/src/Control/Carrier/FossaApiClient/Internal/FossaAPIV1.hs index 3126640fa6..862f937da6 100644 --- a/src/Control/Carrier/FossaApiClient/Internal/FossaAPIV1.hs +++ b/src/Control/Carrier/FossaApiClient/Internal/FossaAPIV1.hs @@ -63,6 +63,17 @@ module Control.Carrier.FossaApiClient.Internal.FossaAPIV1 ( -- * Team getTeams, + addTeamProjects, + + -- * Project + updateProject, + getProjectV2, + + -- * Revision + updateRevision, + + -- * Label + getOrgLabels, ) where import App.Docs (fossaSslCertDocsUrl) @@ -151,8 +162,6 @@ import Fossa.API.Types ( ArchiveComponents (ArchiveComponents), Build, Contributors, - CreateReleaseGroupRequest, - CreateReleaseGroupResponse, CustomBuildUploadPermissions, Issues, OrgId, @@ -160,19 +169,16 @@ import Fossa.API.Types ( PathDependencyFinalizeReq (..), PathDependencyUpload, PathDependencyUploadReq (..), - Policy (..), Project, - ReleaseGroup, - ReleaseGroupRelease, RevisionDependencyCache, SignedURL (signedURL), SignedURLWithKey (surlwkKey, surlwkSignedURL), - Team, TokenTypeResponse, - UpdateReleaseRequest, UploadResponse, useApiOpts, ) + +import Fossa.API.CoreTypes qualified as CoreTypes import Network.HTTP.Client (responseStatus) import Network.HTTP.Client qualified as C import Network.HTTP.Client qualified as HTTP @@ -185,6 +191,7 @@ import Network.HTTP.Req ( MonadHttp (..), NoReqBody (NoReqBody), Option, + PATCH (PATCH), POST (POST), PUT (PUT), ReqBodyBs (ReqBodyBs), @@ -1634,7 +1641,7 @@ getPolicies :: , Has Debug sig m ) => ApiOpts -> - m [Fossa.API.Types.Policy] + m [CoreTypes.Policy] getPolicies apiOpts = fossaReq $ do (baseUrl, baseOpts) <- useApiOpts apiOpts resp <- @@ -1651,7 +1658,7 @@ getTeams :: , Has Debug sig m ) => ApiOpts -> - m [Fossa.API.Types.Team] + m [CoreTypes.Team] getTeams apiOpts = fossaReq $ do (baseUrl, baseOpts) <- useApiOpts apiOpts resp <- @@ -1659,6 +1666,25 @@ getTeams apiOpts = fossaReq $ do req GET (teamsURLEndpoint baseUrl) NoReqBody jsonResponse baseOpts pure (responseBody resp) +addTeamProjectsURLEndpoint :: Url 'Https -> Text -> Url 'Https +addTeamProjectsURLEndpoint baseUrl teamId = baseUrl /: "api" /: "teams" /: teamId /: "projects" + +addTeamProjects :: + ( Has (Lift IO) sig m + , Has Diagnostics sig m + , Has Debug sig m + ) => + ApiOpts -> + Int -> + CoreTypes.AddTeamProjectsRequest -> + m CoreTypes.AddTeamProjectsResponse +addTeamProjects apiOpts teamId addProjectsReq = fossaReq $ do + (baseUrl, baseOpts) <- useApiOpts apiOpts + resp <- + context "Adding projects to team" $ + req PUT (addTeamProjectsURLEndpoint baseUrl $ toText teamId) (ReqBodyJson addProjectsReq) jsonResponse baseOpts + pure (responseBody resp) + deleteReleaseGroupURLEndpoint :: Url 'Https -> Text -> Url 'Https deleteReleaseGroupURLEndpoint baseUrl releaseGroupId = baseUrl /: "api" /: "project_group" /: releaseGroupId @@ -1702,8 +1728,8 @@ updateReleaseGroupRelease :: ApiOpts -> Int -> Int -> - UpdateReleaseRequest -> - m ReleaseGroupRelease + CoreTypes.UpdateReleaseRequest -> + m CoreTypes.ReleaseGroupRelease updateReleaseGroupRelease apiOpts releaseGroupId releaseId updateReq = fossaReq $ do (baseUrl, baseOpts) <- useApiOpts apiOpts resp <- @@ -1720,8 +1746,8 @@ createReleaseGroup :: , Has Debug sig m ) => ApiOpts -> - CreateReleaseGroupRequest -> - m CreateReleaseGroupResponse + CoreTypes.CreateReleaseGroupRequest -> + m CoreTypes.CreateReleaseGroupResponse createReleaseGroup apiOpts createReleaseGroupReq = fossaReq $ do (baseUrl, baseOpts) <- useApiOpts apiOpts resp <- @@ -1735,7 +1761,7 @@ getReleaseGroups :: , Has Debug sig m ) => ApiOpts -> - m [ReleaseGroup] + m [CoreTypes.ReleaseGroup] getReleaseGroups apiOpts = fossaReq $ do (baseUrl, baseOpts) <- useApiOpts apiOpts resp <- @@ -1753,7 +1779,7 @@ getReleaseGroupReleases :: ) => ApiOpts -> Int -> - m [ReleaseGroupRelease] + m [CoreTypes.ReleaseGroupRelease] getReleaseGroupReleases apiOpts releaseGroupId = fossaReq $ do (baseUrl, baseOpts) <- useApiOpts apiOpts resp <- @@ -1769,10 +1795,84 @@ createReleaseGroupRelease :: ApiOpts -> Int -> ReleaseGroupReleaseRevision -> - m ReleaseGroupRelease + m CoreTypes.ReleaseGroupRelease createReleaseGroupRelease apiOpts releaseGroupId createReleaseReq = fossaReq $ do (baseUrl, baseOpts) <- useApiOpts apiOpts resp <- context "Creating release group release" $ req POST (releaseGroupReleaseURLEndpoint baseUrl $ toText releaseGroupId) (ReqBodyJson createReleaseReq) jsonResponse baseOpts pure (responseBody resp) + +updateProjectURLEndpoint :: Url 'Https -> Text -> Url 'Https +updateProjectURLEndpoint baseUrl locator = baseUrl /: "api" /: "projects" /: locator + +updateProject :: + ( Has (Lift IO) sig m + , Has Diagnostics sig m + , Has Debug sig m + ) => + ApiOpts -> + Text -> + CoreTypes.UpdateProjectRequest -> + m CoreTypes.Project +updateProject apiOpts locator updateReq = fossaReq $ do + (baseUrl, baseOpts) <- useApiOpts apiOpts + resp <- + context "Updating project" $ + req PUT (updateProjectURLEndpoint baseUrl locator) (ReqBodyJsonCompat updateReq) jsonResponse baseOpts + pure (responseBody resp) + +updateRevisionURLEndpoint :: Url 'Https -> Text -> Url 'Https +updateRevisionURLEndpoint baseUrl revisionLocator = baseUrl /: "api" /: "revisions" /: revisionLocator + +updateRevision :: + ( Has (Lift IO) sig m + , Has Diagnostics sig m + , Has Debug sig m + ) => + ApiOpts -> + Text -> + CoreTypes.UpdateRevisionRequest -> + m CoreTypes.Revision +updateRevision apiOpts revisionLocator updateReq = fossaReq $ do + (baseUrl, baseOpts) <- useApiOpts apiOpts + resp <- + context "Updating revision" $ + req PATCH (updateRevisionURLEndpoint baseUrl revisionLocator) (ReqBodyJson updateReq) jsonResponse baseOpts + pure (responseBody resp) + +-- Get project using Core's main endpoint +getProjectV2URLEndpoint :: Url 'Https -> Text -> Url 'Https +getProjectV2URLEndpoint baseUrl locator = baseUrl /: "api" /: "projects" /: locator + +getProjectV2 :: + ( Has (Lift IO) sig m + , Has Diagnostics sig m + , Has Debug sig m + ) => + ApiOpts -> + Text -> + m CoreTypes.Project +getProjectV2 apiOpts locator = fossaReq $ do + (baseUrl, baseOpts) <- useApiOpts apiOpts + resp <- + context "Get project" $ + req GET (getProjectV2URLEndpoint baseUrl locator) NoReqBody jsonResponse baseOpts + pure (responseBody resp) + +getOrgLabelsURLEndpoint :: Url 'Https -> Url 'Https +getOrgLabelsURLEndpoint baseUrl = baseUrl /: "api" /: "organizations" /: "labels" + +getOrgLabels :: + ( Has (Lift IO) sig m + , Has Diagnostics sig m + , Has Debug sig m + ) => + ApiOpts -> + m CoreTypes.Labels +getOrgLabels apiOpts = fossaReq $ do + (baseUrl, baseOpts) <- useApiOpts apiOpts + resp <- + context "Retrieving organization labels" $ + req GET (getOrgLabelsURLEndpoint baseUrl) NoReqBody jsonResponse baseOpts + pure (responseBody resp) diff --git a/src/Control/Effect/FossaApiClient.hs b/src/Control/Effect/FossaApiClient.hs index c6fbbd691f..1db6cb7127 100644 --- a/src/Control/Effect/FossaApiClient.hs +++ b/src/Control/Effect/FossaApiClient.hs @@ -5,6 +5,7 @@ module Control.Effect.FossaApiClient ( FossaApiClientF (..), FossaApiClient, addFilesToVsiScan, + addTeamProjects, assertRevisionBinaries, assertUserDefinedBinaries, completeVsiScan, @@ -50,6 +51,10 @@ module Control.Effect.FossaApiClient ( getReleaseGroups, getReleaseGroupReleases, updateReleaseGroupRelease, + getProjectV2, + updateProject, + updateRevision, + getOrgLabels, ) where import App.Fossa.Config.Report (ReportOutputFormat) @@ -70,7 +75,26 @@ import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as NE import Data.Map (Map) import Data.Text (Text) -import Fossa.API.Types (AnalyzedPathDependency, ApiOpts, Archive, ArchiveComponents, Build, Contributors, CreateReleaseGroupRequest, CreateReleaseGroupResponse, CustomBuildUploadPermissions, Issues, Organization, PathDependencyUpload, Policy, Project, ReleaseGroup, ReleaseGroupRelease, RevisionDependencyCache, SignedURL, Team, TokenTypeResponse, UpdateReleaseRequest, UploadResponse) +import Fossa.API.Types ( + AnalyzedPathDependency, + ApiOpts, + Archive, + ArchiveComponents, + Build, + Contributors, + CustomBuildUploadPermissions, + Issues, + Organization, + PathDependencyUpload, + Project, + RevisionDependencyCache, + SignedURL, + TokenTypeResponse, + UploadResponse, + ) + +import Fossa.API.CoreTypes qualified as CoreTypes + import Path (File, Path, Rel) import Srclib.Types (FullSourceUnit, LicenseSourceUnit, Locator, SourceUnit) @@ -85,6 +109,7 @@ data PackageRevision = PackageRevision -- Note: If you add an entry here, please add a corresponding entry in @Test.MockApi.matchExpectation@. data FossaApiClientF a where AddFilesToVsiScan :: VSI.ScanID -> Map (Path Rel File) Fingerprint.Combined -> FossaApiClientF () + AddTeamProjects :: Int -> CoreTypes.AddTeamProjectsRequest -> FossaApiClientF CoreTypes.AddTeamProjectsResponse AssertRevisionBinaries :: Locator -> [Fingerprint Raw] -> FossaApiClientF () AssertUserDefinedBinaries :: IAT.UserDefinedAssertionMeta -> [Fingerprint Raw] -> FossaApiClientF () CompleteVsiScan :: VSI.ScanID -> FossaApiClientF () @@ -102,9 +127,9 @@ data FossaApiClientF a where GetRevisionDependencyCacheStatus :: ProjectRevision -> FossaApiClientF RevisionDependencyCache GetLatestBuild :: ProjectRevision -> FossaApiClientF Build GetOrganization :: FossaApiClientF Organization - GetPolicies :: FossaApiClientF [Policy] + GetPolicies :: FossaApiClientF [CoreTypes.Policy] GetProject :: ProjectRevision -> FossaApiClientF Project - GetTeams :: FossaApiClientF [Team] + GetTeams :: FossaApiClientF [CoreTypes.Team] GetAnalyzedRevisions :: NonEmpty VendoredDependency -> FossaApiClientF [Text] GetAnalyzedPathRevisions :: ProjectRevision -> FossaApiClientF [AnalyzedPathDependency] GetSignedFirstPartyScanUrl :: PackageRevision -> FossaApiClientF SignedURL @@ -143,11 +168,15 @@ data FossaApiClientF a where UploadBuildForReachability :: ProjectRevision -> ProjectMetadata -> [SourceUnitReachability] -> FossaApiClientF () DeleteReleaseGroup :: Int -> FossaApiClientF () DeleteReleaseGroupRelease :: Int -> Int -> FossaApiClientF () - CreateReleaseGroup :: CreateReleaseGroupRequest -> FossaApiClientF CreateReleaseGroupResponse - CreateReleaseGroupRelease :: Int -> ReleaseGroupReleaseRevision -> FossaApiClientF ReleaseGroupRelease - UpdateReleaseGroupRelease :: Int -> Int -> UpdateReleaseRequest -> FossaApiClientF ReleaseGroupRelease - GetReleaseGroups :: FossaApiClientF [ReleaseGroup] - GetReleaseGroupReleases :: Int -> FossaApiClientF [ReleaseGroupRelease] + CreateReleaseGroup :: CoreTypes.CreateReleaseGroupRequest -> FossaApiClientF CoreTypes.CreateReleaseGroupResponse + CreateReleaseGroupRelease :: Int -> ReleaseGroupReleaseRevision -> FossaApiClientF CoreTypes.ReleaseGroupRelease + UpdateReleaseGroupRelease :: Int -> Int -> CoreTypes.UpdateReleaseRequest -> FossaApiClientF CoreTypes.ReleaseGroupRelease + GetReleaseGroups :: FossaApiClientF [CoreTypes.ReleaseGroup] + GetReleaseGroupReleases :: Int -> FossaApiClientF [CoreTypes.ReleaseGroupRelease] + GetProjectV2 :: Text -> FossaApiClientF CoreTypes.Project + UpdateProject :: Text -> CoreTypes.UpdateProjectRequest -> FossaApiClientF CoreTypes.Project + UpdateRevision :: Text -> CoreTypes.UpdateRevisionRequest -> FossaApiClientF CoreTypes.Revision + GetOrgLabels :: FossaApiClientF CoreTypes.Labels deriving instance Show (FossaApiClientF a) @@ -274,29 +303,44 @@ uploadContentForReachability = sendSimple . UploadContentForReachability uploadBuildForReachability :: (Has FossaApiClient sig m) => ProjectRevision -> ProjectMetadata -> [SourceUnitReachability] -> m () uploadBuildForReachability rev revMetadata units = sendSimple $ UploadBuildForReachability rev revMetadata units -getPolicies :: Has FossaApiClient sig m => m [Policy] +getPolicies :: Has FossaApiClient sig m => m [CoreTypes.Policy] getPolicies = sendSimple GetPolicies -getTeams :: Has FossaApiClient sig m => m [Team] +getTeams :: Has FossaApiClient sig m => m [CoreTypes.Team] getTeams = sendSimple GetTeams +addTeamProjects :: Has FossaApiClient sig m => Int -> CoreTypes.AddTeamProjectsRequest -> m CoreTypes.AddTeamProjectsResponse +addTeamProjects teamId req = sendSimple $ AddTeamProjects teamId req + deleteReleaseGroup :: Has FossaApiClient sig m => Int -> m () deleteReleaseGroup releaseGroupId = sendSimple $ DeleteReleaseGroup releaseGroupId deleteReleaseGroupRelease :: Has FossaApiClient sig m => Int -> Int -> m () deleteReleaseGroupRelease releaseGroupId releaseId = sendSimple $ DeleteReleaseGroupRelease releaseGroupId releaseId -updateReleaseGroupRelease :: Has FossaApiClient sig m => Int -> Int -> UpdateReleaseRequest -> m ReleaseGroupRelease +updateReleaseGroupRelease :: Has FossaApiClient sig m => Int -> Int -> CoreTypes.UpdateReleaseRequest -> m CoreTypes.ReleaseGroupRelease updateReleaseGroupRelease releaseGroupId releaseId updateReq = sendSimple $ UpdateReleaseGroupRelease releaseGroupId releaseId updateReq -createReleaseGroup :: Has FossaApiClient sig m => CreateReleaseGroupRequest -> m CreateReleaseGroupResponse +createReleaseGroup :: Has FossaApiClient sig m => CoreTypes.CreateReleaseGroupRequest -> m CoreTypes.CreateReleaseGroupResponse createReleaseGroup req = sendSimple $ CreateReleaseGroup req -createReleaseGroupRelease :: Has FossaApiClient sig m => Int -> ReleaseGroupReleaseRevision -> m ReleaseGroupRelease +createReleaseGroupRelease :: Has FossaApiClient sig m => Int -> ReleaseGroupReleaseRevision -> m CoreTypes.ReleaseGroupRelease createReleaseGroupRelease releaseGroupId req = sendSimple $ CreateReleaseGroupRelease releaseGroupId req -getReleaseGroups :: Has FossaApiClient sig m => m [ReleaseGroup] +getReleaseGroups :: Has FossaApiClient sig m => m [CoreTypes.ReleaseGroup] getReleaseGroups = sendSimple GetReleaseGroups -getReleaseGroupReleases :: Has FossaApiClient sig m => Int -> m [ReleaseGroupRelease] +getReleaseGroupReleases :: Has FossaApiClient sig m => Int -> m [CoreTypes.ReleaseGroupRelease] getReleaseGroupReleases releaseGroupId = sendSimple $ GetReleaseGroupReleases releaseGroupId + +getProjectV2 :: Has FossaApiClient sig m => Text -> m CoreTypes.Project +getProjectV2 locator = sendSimple $ GetProjectV2 locator + +updateProject :: Has FossaApiClient sig m => Text -> CoreTypes.UpdateProjectRequest -> m CoreTypes.Project +updateProject locator req = sendSimple $ UpdateProject locator req + +updateRevision :: Has FossaApiClient sig m => Text -> CoreTypes.UpdateRevisionRequest -> m CoreTypes.Revision +updateRevision revisionLocator req = sendSimple $ UpdateRevision revisionLocator req + +getOrgLabels :: Has FossaApiClient sig m => m CoreTypes.Labels +getOrgLabels = sendSimple GetOrgLabels diff --git a/src/Fossa/API/CoreTypes.hs b/src/Fossa/API/CoreTypes.hs new file mode 100644 index 0000000000..45ccf1b869 --- /dev/null +++ b/src/Fossa/API/CoreTypes.hs @@ -0,0 +1,298 @@ +{-# LANGUAGE RecordWildCards #-} + +module Fossa.API.CoreTypes ( + Project (..), + Policy (..), + Team (..), + PolicyType (..), + ReleaseGroup (..), + ReleaseGroupRelease (..), + ReleaseProject (..), + UpdateReleaseProjectRequest (..), + UpdateReleaseRequest (..), + CreateReleaseGroupRequest (..), + CreateReleaseGroupResponse (..), + UpdateProjectRequest (..), + AddTeamProjectsRequest (..), + AddTeamProjectsResponse (..), + TeamProjectAction (..), + UpdateRevisionRequest (..), + Label (..), + Labels (..), + Revision (..), +) where + +import App.Types (ReleaseGroupReleaseRevision) +import Data.Aeson ( + FromJSON (parseJSON), + KeyValue ((.=)), + ToJSON (toJSON), + Value (Null), + object, + withObject, + withText, + (.:), + ) +import Data.Text (Text) + +--- | Data types of Core's main endpoints + +-- Policies +data Policy = Policy + { policyId :: Int + , policyTitle :: Text + , policyType :: PolicyType + } + deriving (Eq, Ord, Show) + +instance FromJSON Policy where + parseJSON = withObject "Policy" $ \obj -> + Policy + <$> obj .: "id" + <*> obj .: "title" + <*> obj .: "type" + +instance FromJSON PolicyType where + parseJSON = withText "PolicyType" $ \case + "LICENSING" -> pure LICENSING + "SECURITY" -> pure SECURITY + "QUALITY" -> pure QUALITY + other -> pure $ PolicyUnknown other + +data PolicyType + = LICENSING + | SECURITY + | QUALITY + | PolicyUnknown Text + deriving (Eq, Ord, Show) + +-- Teams +data Team = Team + { teamId :: Int + , teamName :: Text + } + deriving (Eq, Ord, Show) + +instance FromJSON Team where + parseJSON = withObject "Team" $ \obj -> + Team + <$> obj .: "id" + <*> obj .: "name" + +data TeamProjectAction = Add + deriving (Eq, Ord, Show) + +instance ToJSON TeamProjectAction where + toJSON Add = "add" + +data AddTeamProjectsRequest = AddTeamProjectsRequest + { projectLocators :: [Text] + , action :: TeamProjectAction + } + deriving (Eq, Ord, Show) + +instance ToJSON AddTeamProjectsRequest where + toJSON AddTeamProjectsRequest{..} = + object + [ "projects" .= projectLocators + , "action" .= action + ] + +newtype AddTeamProjectsResponse = AddTeamProjectsResponse {teamProjectLocators :: [Text]} + deriving (Eq, Ord, Show) + +instance FromJSON AddTeamProjectsResponse where + parseJSON = withObject "AddTeamProjectsResponse" $ \obj -> + AddTeamProjectsResponse + <$> obj .: "projects" + +-- ReleaseGroup +data ReleaseGroup = ReleaseGroup + { releaseGroupId :: Int + , releaseGroupTitle :: Text + , releaseGroupReleases :: [ReleaseGroupRelease] + } + deriving (Eq, Ord, Show) + +instance FromJSON ReleaseGroup where + parseJSON = withObject "ReleaseGroup" $ \obj -> + ReleaseGroup + <$> obj .: "id" + <*> obj .: "title" + <*> obj .: "releases" + +data ReleaseGroupRelease = ReleaseGroupRelease + { releaseGroupReleaseId :: Int + , releaseGroupReleaseTitle :: Text + , releaseGroupReleaseProjects :: [ReleaseProject] + } + deriving (Eq, Ord, Show) + +instance FromJSON ReleaseGroupRelease where + parseJSON = withObject "ReleaseGroupRelease" $ \obj -> + ReleaseGroupRelease + <$> obj .: "id" + <*> obj .: "title" + <*> obj .: "projects" + +data ReleaseProject = ReleaseProject + { releaseProjectLocator :: Text + , releaseProjectRevisionId :: Text + , releaseProjectBranch :: Text + } + deriving (Eq, Ord, Show) + +instance FromJSON ReleaseProject where + parseJSON = withObject "ReleaseProject" $ \obj -> + ReleaseProject + <$> obj .: "projectId" + <*> obj .: "revisionId" + <*> obj .: "branch" + +data UpdateReleaseRequest = UpdateReleaseRequest + { updatedReleaseTitle :: Text + , updatedProjects :: [UpdateReleaseProjectRequest] + } + deriving (Eq, Ord, Show) + +instance ToJSON UpdateReleaseRequest where + toJSON UpdateReleaseRequest{..} = + object + [ "title" .= updatedReleaseTitle + , "projects" .= updatedProjects + ] + +data UpdateReleaseProjectRequest = UpdateReleaseProjectRequest + { updateReleaseProjectLocator :: Text + , updateReleaseProjectRevisionId :: Text + , updateReleaseProjectBranch :: Text + , -- Existence of this field signifies to core that the project exists and the release project just needs to be updated. + -- If this field is empty it means that we are adding a new project to the release. + targetReleaseGroupId :: Maybe Int + } + deriving (Eq, Ord, Show) + +instance ToJSON UpdateReleaseProjectRequest where + toJSON UpdateReleaseProjectRequest{..} = + object + [ "projectId" .= updateReleaseProjectLocator + , "revisionId" .= updateReleaseProjectRevisionId + , "branch" .= updateReleaseProjectBranch + , "projectGroupReleaseId" .= maybe Null toJSON targetReleaseGroupId + ] + +data CreateReleaseGroupRequest = CreateReleaseGroupRequest + { title :: Text + , release :: ReleaseGroupReleaseRevision + , maybeLicensePolicyId :: Maybe Int + , maybeSecurityPolicyId :: Maybe Int + , maybeQualityPolicyId :: Maybe Int + , maybeTeamIds :: Maybe [Int] + } + deriving (Eq, Ord, Show) + +instance ToJSON CreateReleaseGroupRequest where + toJSON CreateReleaseGroupRequest{..} = + object + [ "title" .= title + , "release" .= release + , "licensingPolicyId" .= maybe Null toJSON maybeLicensePolicyId + , "securityPolicyId" .= maybe Null toJSON maybeSecurityPolicyId + , "qualityPolicyId" .= maybe Null toJSON maybeQualityPolicyId + , "teams" .= maybe Null toJSON maybeTeamIds + ] + +newtype CreateReleaseGroupResponse = CreateReleaseGroupResponse {groupId :: Int} + deriving (Eq, Ord, Show) + +instance FromJSON CreateReleaseGroupResponse where + parseJSON = withObject "CreateReleaseGroupResponse" $ \obj -> + CreateReleaseGroupResponse + <$> obj .: "id" + +-- Project +data Project = Project + { projectLocator :: Text + , projectTitle :: Text + , projectUrl :: Maybe Text + , projectIssueTrackerIds :: Maybe [Text] + , projectPolicyId :: Maybe Int + , projectLatestRevision :: Maybe Text + , projectDefaultBranch :: Maybe Text + } + deriving (Eq, Ord, Show) + +instance FromJSON Project where + parseJSON = withObject "ProjectResponse" $ \obj -> + Project + <$> obj .: "locator" + <*> obj .: "title" + <*> obj .: "url" + <*> obj .: "issueTrackerProjectIds" + <*> obj .: "policyId" + <*> obj .: "last_analyzed_revision" + <*> obj .: "default_branch" + +data UpdateProjectRequest = UpdateProjectRequest + { updateProjectTitle :: Maybe Text + , updateProjectUrl :: Maybe Text + , updateProjectIssueTrackerIds :: Maybe [Text] + , updateProjectLabelIds :: Maybe [Int] + , updateProjectPolicyId :: Maybe Int + , -- This field needs to be set, otherwise the default branch will be removed + updateProjectDefaultBranch :: Maybe Text + } + deriving (Eq, Ord, Show) + +instance ToJSON UpdateProjectRequest where + toJSON UpdateProjectRequest{..} = + object + [ "title" .= maybe Null toJSON updateProjectTitle + , "url" .= maybe Null toJSON updateProjectUrl + , "issueTrackerProjectIds" .= maybe Null toJSON updateProjectIssueTrackerIds + , "labels" .= maybe Null toJSON updateProjectLabelIds + , "policyId" .= maybe Null toJSON updateProjectPolicyId + , "default_branch" .= maybe Null toJSON updateProjectDefaultBranch + ] + +-- Revision +data Revision = Revision + { revisionLocator :: Text + , revisionLink :: Maybe Text + } + deriving (Eq, Ord, Show) + +instance FromJSON Revision where + parseJSON = withObject "Revision" $ \obj -> + Revision + <$> obj .: "locator" + <*> obj .: "link" + +newtype UpdateRevisionRequest = UpdateRevisionRequest {newRevisionLink :: Text} + deriving (Eq, Ord, Show) + +instance ToJSON UpdateRevisionRequest where + toJSON UpdateRevisionRequest{..} = + object + ["link" .= newRevisionLink] + +-- Label +data Label = Label + { labelId :: Int + , labelName :: Text + } + deriving (Eq, Ord, Show) + +instance FromJSON Label where + parseJSON = withObject "Label " $ \obj -> + Label + <$> obj .: "id" + <*> obj .: "label" + +newtype Labels = Labels {labels :: [Label]} + deriving (Eq, Ord, Show) + +instance FromJSON Labels where + parseJSON = withObject "Labels" $ \obj -> + Labels + <$> obj .: "labels" diff --git a/src/Fossa/API/Types.hs b/src/Fossa/API/Types.hs index 8daeb4969c..34153e4223 100644 --- a/src/Fossa/API/Types.hs +++ b/src/Fossa/API/Types.hs @@ -38,23 +38,13 @@ module Fossa.API.Types ( CustomBuildUploadPermissions (..), ProjectPermissionStatus (..), ReleaseGroupPermissionStatus (..), - Policy (..), - Team (..), - PolicyType (..), - ReleaseGroup (..), - ReleaseGroupRelease (..), - ReleaseProject (..), - UpdateReleaseProjectRequest (..), - UpdateReleaseRequest (..), - CreateReleaseGroupRequest (..), - CreateReleaseGroupResponse (..), useApiOpts, defaultApiPollDelay, blankOrganization, ) where import App.Fossa.Lernie.Types (GrepEntry) -import App.Types (FullFileUploads (..), ReleaseGroupReleaseRevision, fullFileUploadsToCliLicenseScanType) +import App.Types (FullFileUploads (..), fullFileUploadsToCliLicenseScanType) import Control.Applicative ((<|>)) import Control.Effect.Diagnostics (Diagnostics, Has, fatalText) import Control.Timeout (Duration (Seconds)) @@ -62,7 +52,7 @@ import Data.Aeson ( FromJSON (parseJSON), KeyValue ((.=)), ToJSON (toJSON), - Value (Null, String), + Value (String), object, withObject, withText, @@ -101,6 +91,8 @@ import Text.URI.QQ (uri) import Types (ArchiveUploadType (..)) import Unsafe.Coerce qualified as Unsafe +--- | Data types of CLI owned endpoints in Core + newtype ApiKey = ApiKey {unApiKey :: Text} deriving (Eq, Ord) @@ -954,149 +946,3 @@ instance FromJSON AnalyzedPathDependency where <$> obj .: "path" <*> obj .: "id" <*> obj .: "version" - ---- Policies -data Policy = Policy - { policyId :: Int - , policyTitle :: Text - , policyType :: PolicyType - } - deriving (Eq, Ord, Show) - -instance FromJSON Policy where - parseJSON = withObject "Policy" $ \obj -> - Policy - <$> obj .: "id" - <*> obj .: "title" - <*> obj .: "type" - -instance FromJSON PolicyType where - parseJSON = withText "PolicyType" $ \case - "LICENSING" -> pure LICENSING - "SECURITY" -> pure SECURITY - "QUALITY" -> pure QUALITY - other -> pure $ PolicyUnknown other - -data PolicyType - = LICENSING - | SECURITY - | QUALITY - | PolicyUnknown Text - deriving (Eq, Ord, Show) - --- Teams -data Team = Team - { teamId :: Int - , teamName :: Text - } - deriving (Eq, Ord, Show) - -instance FromJSON Team where - parseJSON = withObject "Team" $ \obj -> - Team - <$> obj .: "id" - <*> obj .: "name" - --- ReleaseGroup -data ReleaseGroup = ReleaseGroup - { releaseGroupId :: Int - , releaseGroupTitle :: Text - , releaseGroupReleases :: [ReleaseGroupRelease] - } - deriving (Eq, Ord, Show) - -instance FromJSON ReleaseGroup where - parseJSON = withObject "ReleaseGroup" $ \obj -> - ReleaseGroup - <$> obj .: "id" - <*> obj .: "title" - <*> obj .: "releases" - -data ReleaseGroupRelease = ReleaseGroupRelease - { releaseGroupReleaseId :: Int - , releaseGroupReleaseTitle :: Text - , releaseGroupReleaseProjects :: [ReleaseProject] - } - deriving (Eq, Ord, Show) - -instance FromJSON ReleaseGroupRelease where - parseJSON = withObject "ReleaseGroupRelease" $ \obj -> - ReleaseGroupRelease - <$> obj .: "id" - <*> obj .: "title" - <*> obj .: "projects" - -data ReleaseProject = ReleaseProject - { releaseProjectLocator :: Text - , releaseProjectRevisionId :: Text - , releaseProjectBranch :: Text - } - deriving (Eq, Ord, Show) - -instance FromJSON ReleaseProject where - parseJSON = withObject "ReleaseProject" $ \obj -> - ReleaseProject - <$> obj .: "projectId" - <*> obj .: "revisionId" - <*> obj .: "branch" - -data UpdateReleaseRequest = UpdateReleaseRequest - { updatedReleaseTitle :: Text - , updatedProjects :: [UpdateReleaseProjectRequest] - } - deriving (Eq, Ord, Show) - -instance ToJSON UpdateReleaseRequest where - toJSON UpdateReleaseRequest{..} = - object - [ "title" .= updatedReleaseTitle - , "projects" .= updatedProjects - ] - -data UpdateReleaseProjectRequest = UpdateReleaseProjectRequest - { updateReleaseProjectLocator :: Text - , updateReleaseProjectRevisionId :: Text - , updateReleaseProjectBranch :: Text - , -- Existence of this field signifies to core that the project exists and the release project just needs to be updated. - -- If this field is empty it means that we are adding a new project to the release. - targetReleaseGroupId :: Maybe Int - } - deriving (Eq, Ord, Show) - -instance ToJSON UpdateReleaseProjectRequest where - toJSON UpdateReleaseProjectRequest{..} = - object - [ "projectId" .= updateReleaseProjectLocator - , "revisionId" .= updateReleaseProjectRevisionId - , "branch" .= updateReleaseProjectBranch - , "projectGroupReleaseId" .= maybe Null toJSON targetReleaseGroupId - ] - -data CreateReleaseGroupRequest = CreateReleaseGroupRequest - { title :: Text - , release :: ReleaseGroupReleaseRevision - , maybeLicensePolicyId :: Maybe Int - , maybeSecurityPolicyId :: Maybe Int - , maybeQualityPolicyId :: Maybe Int - , maybeTeamIds :: Maybe [Int] - } - deriving (Eq, Ord, Show) - -instance ToJSON CreateReleaseGroupRequest where - toJSON CreateReleaseGroupRequest{..} = - object - [ "title" .= title - , "release" .= release - , "licensingPolicyId" .= maybe Null toJSON maybeLicensePolicyId - , "securityPolicyId" .= maybe Null toJSON maybeSecurityPolicyId - , "qualityPolicyId" .= maybe Null toJSON maybeQualityPolicyId - , "teams" .= maybe Null toJSON maybeTeamIds - ] - -newtype CreateReleaseGroupResponse = CreateReleaseGroupResponse {groupId :: Int} - deriving (Eq, Ord, Show) - -instance FromJSON CreateReleaseGroupResponse where - parseJSON = withObject "CreateReleaseGroupResponse" $ \obj -> - CreateReleaseGroupResponse - <$> obj .: "id" diff --git a/test/App/Fossa/ApiUtilsSpec.hs b/test/App/Fossa/ApiUtilsSpec.hs new file mode 100644 index 0000000000..814ab325c2 --- /dev/null +++ b/test/App/Fossa/ApiUtilsSpec.hs @@ -0,0 +1,84 @@ +module App.Fossa.ApiUtilsSpec ( + spec, +) where + +import App.Fossa.ApiUtils (retrieveLabelIds, retrievePolicyId, retrieveTeamIds, retrieveTeamIdsWithMaybe) +import Fossa.API.CoreTypes (Label (..), Labels (..), PolicyType (..), Team (..)) +import Test.Effect (expectFatal', it', shouldBe') +import Test.Fixtures qualified as Fixtures +import Test.Hspec (Spec, describe) + +spec :: Spec +spec = do + retrievePolicyIdSpec + retrieveTeamIdsWithMaybeSpec + retrieveTeamIdsSpec + retrieveLabelIdsSpec + +retrievePolicyIdSpec :: Spec +retrievePolicyIdSpec = do + describe "retrievePolicyId" $ do + it' "should fail when the policy title does not exist" $ do + let policies = [Fixtures.policy] + expectFatal' $ retrievePolicyId (Just "non-existent-policy-name") LICENSING policies + + it' "should fail when the policy type does not match" $ do + let policies = [Fixtures.policy] + expectFatal' $ retrievePolicyId (Just "example-policy") SECURITY policies + + it' "should fail when there are multiple policies with the same name" $ do + let policies = [Fixtures.policy, Fixtures.policy] + expectFatal' $ retrievePolicyId (Just "example-policy") SECURITY policies + + it' "should return Nothing" $ do + let policies = [Fixtures.policy] + res <- retrievePolicyId Nothing LICENSING policies + res `shouldBe'` Nothing + + it' "should successfully retrieve policy id" $ do + let policies = [Fixtures.policy] + res <- retrievePolicyId (Just "example-policy") LICENSING policies + res `shouldBe'` Just 7 + +retrieveTeamIdsWithMaybeSpec :: Spec +retrieveTeamIdsWithMaybeSpec = do + describe "retrieveTeamIdsWithMaybe" $ do + it' "should return Nothing if the team names is Nothing" $ do + let teams = [Fixtures.team, Fixtures.team{teamId = 11, teamName = "example-team-2"}] + res <- retrieveTeamIdsWithMaybe Nothing teams + res `shouldBe'` Nothing + + it' "should successfully retrieve all team ids" $ do + let teams = [Fixtures.team, Fixtures.team{teamId = 11, teamName = "example-team-2"}] + res <- retrieveTeamIdsWithMaybe (Just ["example-team", "example-team-2"]) teams + res `shouldBe'` Just [10, 11] + +retrieveTeamIdsSpec :: Spec +retrieveTeamIdsSpec = do + describe "retrieveTeamIds" $ do + it' "should fail if not all team names exist in org" $ do + let teams = [Fixtures.team, Fixtures.team{teamId = 11, teamName = "example-team-2"}] + expectFatal' $ retrieveTeamIds (["example-team-2", "non-existent-team"]) teams + + it' "should successfully retrieve all team ids" $ do + let teams = [Fixtures.team, Fixtures.team{teamId = 11, teamName = "example-team-2"}] + res <- retrieveTeamIds ["example-team", "example-team-2"] teams + res `shouldBe'` [10, 11] + +retrieveLabelIdsSpec :: Spec +retrieveLabelIdsSpec = do + describe "retrieveLabelIds" $ do + let label1 = Label 1 "example-label" + let label2 = Label 2 "example-label-2" + let labels = Labels [label1, label2] + + it' "should return empty list and label warnings when no label names exists Labels" $ do + res <- retrieveLabelIds ["non-existent-label", "non-existent-label-2"] labels + let warning1 = "Label `non-existent-label` does not exist\nNavigate to `Organization Settings` in the FOSSA web UI to create new labels: https://app.fossa.com/account/settings/organization" + warning2 = "Label `non-existent-label-2` does not exist\nNavigate to `Organization Settings` in the FOSSA web UI to create new labels: https://app.fossa.com/account/settings/organization" + expectedWarnings = Just [warning1, warning2] + res `shouldBe'` ([], expectedWarnings) + + it' "should return all labels" $ do + res <- retrieveLabelIds ["example-label", "example-label-2"] labels + res `shouldBe'` ([2, 1], Nothing) diff --git a/test/App/Fossa/Configuration/ConfigurationSpec.hs b/test/App/Fossa/Configuration/ConfigurationSpec.hs index 0a0d9ae1fc..ecc46c6ee7 100644 --- a/test/App/Fossa/Configuration/ConfigurationSpec.hs +++ b/test/App/Fossa/Configuration/ConfigurationSpec.hs @@ -81,10 +81,12 @@ expectedReleaseGroupProject = expectedConfigProject :: ConfigProject expectedConfigProject = ConfigProject - { configProjID = Just "github.com/fossa-cli" + { configProjLocator = Just "custom+1/github.com/fossa-cli" + , configProjID = Just "github.com/fossa-cli" , configName = Just "fossa-cli" , configLink = Just "fossa.com" , configTeam = Just "fossa-team" + , configTeams = Just ["fossa-team"] , configJiraKey = Just "key" , configUrl = Just "fossa.com" , configPolicy = Just (PolicyName "license-policy") diff --git a/test/App/Fossa/Configuration/testdata/valid-default-yaml/.fossa.yaml b/test/App/Fossa/Configuration/testdata/valid-default-yaml/.fossa.yaml index 4a98bf8f48..3f03554518 100644 --- a/test/App/Fossa/Configuration/testdata/valid-default-yaml/.fossa.yaml +++ b/test/App/Fossa/Configuration/testdata/valid-default-yaml/.fossa.yaml @@ -4,9 +4,12 @@ server: https://app.fossa.com apiKey: "123" project: + locator: custom+1/github.com/fossa-cli id: github.com/fossa-cli name: fossa-cli team: fossa-team + teams: + - fossa-team policy: license-policy link: fossa.com url: fossa.com diff --git a/test/App/Fossa/Configuration/testdata/valid-default/.fossa.yml b/test/App/Fossa/Configuration/testdata/valid-default/.fossa.yml index 36c18f0bcd..e28018be14 100644 --- a/test/App/Fossa/Configuration/testdata/valid-default/.fossa.yml +++ b/test/App/Fossa/Configuration/testdata/valid-default/.fossa.yml @@ -4,9 +4,12 @@ server: https://app.fossa.com apiKey: "123" project: + locator: custom+1/github.com/fossa-cli id: github.com/fossa-cli name: fossa-cli team: fossa-team + teams: + - fossa-team policy: license-policy link: fossa.com url: fossa.com diff --git a/test/App/Fossa/Project/EditSpec.hs b/test/App/Fossa/Project/EditSpec.hs new file mode 100644 index 0000000000..5717b455c0 --- /dev/null +++ b/test/App/Fossa/Project/EditSpec.hs @@ -0,0 +1,215 @@ +module App.Fossa.Project.EditSpec ( + spec, + apiOpts', +) where + +import App.Fossa.Config.Project.Edit (EditConfig (..), ProjectIdentifier (..)) +import App.Fossa.Project.Edit (editMain) +import App.Types (Policy (..)) +import Control.Algebra (Has) +import Control.Carrier.Debug (ignoreDebug) +import Control.Effect.FossaApiClient (FossaApiClientF (..)) +import Data.Text (Text) +import Fossa.API.CoreTypes qualified as Types +import Fossa.API.Types (ApiKey (..), ApiOpts (..), OrgId (..), Organization (organizationId), defaultApiPollDelay) +import Test.Effect (expectFatal', it', shouldBe') +import Test.Fixtures qualified as Fixtures +import Test.Hspec (Spec, describe) +import Test.MockApi (MockApi, alwaysReturns, returnsOnce) + +apiOpts' :: ApiOpts +apiOpts' = + ApiOpts + { apiOptsUri = Nothing + , apiOptsApiKey = ApiKey "123" + , apiOptsPollDelay = defaultApiPollDelay + } + +locator :: Text +locator = "custom+123/example-locator" + +projId :: Text +projId = "example-locator" + +revisionLocator :: Text +revisionLocator = locator <> "$789" + +title :: Text +title = "example-title" + +url :: Text +url = "example-url" + +jiraKey :: Text +jiraKey = "example-jira-key" + +link :: Text +link = "example-link" + +policyName :: Text +policyName = "example-policy" + +teamName :: Text +teamName = "example-team" + +teamName2 :: Text +teamName2 = "example-team-2" + +labelName :: Text +labelName = "example-label" + +labelName2 :: Text +labelName2 = "example-label-2" + +editConfig :: EditConfig +editConfig = + EditConfig + { apiOpts = apiOpts' + , projectIdentifier = ProjectLocator locator + , projectTitle = Just title + , projectUrl = Just url + , projectJiraKey = Just jiraKey + , projectLink = Just link + , teams = Just [teamName, teamName2] + , projectPolicy = Just $ PolicyName policyName + , projectLabels = Just [labelName2, labelName] + } + +emptyEditConfig :: EditConfig +emptyEditConfig = + EditConfig + { apiOpts = apiOpts' + , projectIdentifier = ProjectLocator locator + , projectTitle = Nothing + , projectUrl = Nothing + , projectJiraKey = Nothing + , projectLink = Nothing + , teams = Nothing + , projectPolicy = Nothing + , projectLabels = Nothing + } + +updateProjectReq :: Types.UpdateProjectRequest +updateProjectReq = + Types.UpdateProjectRequest + { Types.updateProjectTitle = Just title + , Types.updateProjectUrl = Just url + , Types.updateProjectIssueTrackerIds = Just [jiraKey] + , Types.updateProjectLabelIds = Just [20, 21] + , Types.updateProjectPolicyId = Just 7 + , Types.updateProjectDefaultBranch = Just "main" + } + +updateRevisionReq :: Types.UpdateRevisionRequest +updateRevisionReq = Types.UpdateRevisionRequest{Types.newRevisionLink = link} + +addTeamProjectsReq :: Types.AddTeamProjectsRequest +addTeamProjectsReq = + Types.AddTeamProjectsRequest + { Types.projectLocators = [locator] + , Types.action = Types.Add + } + +addTeamProjectsRes :: Types.AddTeamProjectsResponse +addTeamProjectsRes = Types.AddTeamProjectsResponse{Types.teamProjectLocators = [locator]} + +label1 :: Types.Label +label1 = + Types.Label + { Types.labelId = 20 + , Types.labelName = labelName + } + +label2 :: Types.Label +label2 = + Types.Label + { Types.labelId = 21 + , Types.labelName = labelName2 + } + +labels :: Types.Labels +labels = Types.Labels{Types.labels = [label1, label2]} + +initialProject :: Types.Project +initialProject = + Types.Project + { Types.projectLocator = locator + , Types.projectTitle = "initial-title" + , Types.projectUrl = Nothing + , Types.projectIssueTrackerIds = Just [jiraKey] + , Types.projectPolicyId = Nothing + , Types.projectLatestRevision = Just revisionLocator + , Types.projectDefaultBranch = Just "main" + } + +updatedProject :: Types.Project +updatedProject = + Types.Project + { Types.projectLocator = locator + , Types.projectTitle = title + , Types.projectUrl = Just url + , Types.projectIssueTrackerIds = Nothing + , Types.projectPolicyId = Just 7 + , Types.projectLatestRevision = Just revisionLocator + , Types.projectDefaultBranch = Just "main" + } + +revision :: Types.Revision +revision = + Types.Revision + { Types.revisionLocator = revisionLocator + , Types.revisionLink = Just link + } + +expectUpdateProjectSuccess :: Has MockApi sig m => m () +expectUpdateProjectSuccess = UpdateProject locator updateProjectReq `alwaysReturns` updatedProject + +expectGetOrgSuccess :: Has MockApi sig m => m () +expectGetOrgSuccess = GetOrganization `alwaysReturns` Fixtures.organization{organizationId = OrgId 123} + +expectGetProjectSuccess :: Has MockApi sig m => m () +expectGetProjectSuccess = GetProjectV2 locator `alwaysReturns` initialProject + +expectUpdateRevisionSuccess :: Has MockApi sig m => m () +expectUpdateRevisionSuccess = UpdateRevision revisionLocator updateRevisionReq `alwaysReturns` revision + +expectGetTeamsSuccess :: Has MockApi sig m => m () +expectGetTeamsSuccess = GetTeams `alwaysReturns` [Fixtures.team, Fixtures.team{Types.teamId = 11, Types.teamName = teamName2}] + +expectGetPoliciesSuccess :: Has MockApi sig m => m () +expectGetPoliciesSuccess = GetPolicies `alwaysReturns` [Fixtures.policy] + +expectGetOrgLabelsSuccess :: Has MockApi sig m => m () +expectGetOrgLabelsSuccess = GetOrgLabels `alwaysReturns` labels + +spec :: Spec +spec = do + describe "Edit Project" $ do + it' "should fail when a project does not have a latest revision" $ do + GetProjectV2 locator `returnsOnce` initialProject{Types.projectLatestRevision = Nothing} + expectFatal' $ ignoreDebug $ editMain emptyEditConfig{projectLink = Just link} + + it' "should successfully execute with ProjectIdentifier ProjectLocator" $ do + expectGetProjectSuccess + expectGetPoliciesSuccess + expectGetOrgLabelsSuccess + expectUpdateProjectSuccess + expectGetTeamsSuccess + AddTeamProjects 10 addTeamProjectsReq `returnsOnce` addTeamProjectsRes + AddTeamProjects 11 addTeamProjectsReq `returnsOnce` addTeamProjectsRes + expectUpdateRevisionSuccess + res <- ignoreDebug $ editMain editConfig + res `shouldBe'` () + + it' "should successfully execute with ProjectIdentifier ProjectId" $ do + expectGetOrgSuccess + expectGetProjectSuccess + expectGetPoliciesSuccess + expectGetOrgLabelsSuccess + expectUpdateProjectSuccess + expectGetTeamsSuccess + AddTeamProjects 10 addTeamProjectsReq `returnsOnce` addTeamProjectsRes + AddTeamProjects 11 addTeamProjectsReq `returnsOnce` addTeamProjectsRes + expectUpdateRevisionSuccess + res <- ignoreDebug $ editMain editConfig{projectIdentifier = ProjectId projId} + res `shouldBe'` () diff --git a/test/App/Fossa/ReleaseGroup/AddProjectsSpec.hs b/test/App/Fossa/ReleaseGroup/AddProjectsSpec.hs index fd12004f3e..5e75c757d1 100644 --- a/test/App/Fossa/ReleaseGroup/AddProjectsSpec.hs +++ b/test/App/Fossa/ReleaseGroup/AddProjectsSpec.hs @@ -9,8 +9,8 @@ import App.Fossa.ReleaseGroup.CreateSpec (apiOpts') import Control.Algebra (Has) import Control.Carrier.Debug (ignoreDebug) import Control.Effect.FossaApiClient (FossaApiClientF (..)) -import Fossa.API.Types (UpdateReleaseProjectRequest (..), UpdateReleaseRequest (..)) -import Fossa.API.Types qualified as Types +import Fossa.API.CoreTypes (UpdateReleaseProjectRequest (..), UpdateReleaseRequest (..)) +import Fossa.API.CoreTypes qualified as Types import Test.Effect (expectFatal', it', shouldBe') import Test.Fixtures qualified as Fixtures import Test.Hspec (Spec, describe, it, shouldBe) diff --git a/test/App/Fossa/ReleaseGroup/CommonSpec.hs b/test/App/Fossa/ReleaseGroup/CommonSpec.hs index a100ff8d78..5d15d4ba52 100644 --- a/test/App/Fossa/ReleaseGroup/CommonSpec.hs +++ b/test/App/Fossa/ReleaseGroup/CommonSpec.hs @@ -3,7 +3,7 @@ module App.Fossa.ReleaseGroup.CommonSpec ( ) where import App.Fossa.ReleaseGroup.Common (retrieveReleaseGroupId, retrieveReleaseGroupRelease) -import Fossa.API.Types (ReleaseGroupRelease (..)) +import Fossa.API.CoreTypes (ReleaseGroupRelease (..)) import Test.Effect (expectFatal', it', shouldBe') import Test.Fixtures qualified as Fixtures import Test.Hspec (Spec, describe) diff --git a/test/App/Fossa/ReleaseGroup/CreateReleaseSpec.hs b/test/App/Fossa/ReleaseGroup/CreateReleaseSpec.hs index f077956af1..6cca26a005 100644 --- a/test/App/Fossa/ReleaseGroup/CreateReleaseSpec.hs +++ b/test/App/Fossa/ReleaseGroup/CreateReleaseSpec.hs @@ -9,7 +9,7 @@ import App.Fossa.ReleaseGroup.CreateSpec (apiOpts') import Control.Algebra (Has) import Control.Carrier.Debug (ignoreDebug) import Control.Effect.FossaApiClient (FossaApiClientF (..)) -import Fossa.API.Types qualified as Types +import Fossa.API.CoreTypes qualified as Types import Test.Effect (expectFatal', it', shouldBe') import Test.Fixtures qualified as Fixtures import Test.Hspec (Spec, describe) diff --git a/test/App/Fossa/ReleaseGroup/CreateSpec.hs b/test/App/Fossa/ReleaseGroup/CreateSpec.hs index 100df16861..504f07b368 100644 --- a/test/App/Fossa/ReleaseGroup/CreateSpec.hs +++ b/test/App/Fossa/ReleaseGroup/CreateSpec.hs @@ -5,11 +5,12 @@ module App.Fossa.ReleaseGroup.CreateSpec ( import App.Fossa.Config.ReleaseGroup.Create (CreateConfig (..)) import App.Fossa.Config.ReleaseGroup.CreateSpec (expectedReleaseGroupReleaseRevisionFromConfig, expectedReleaseGroupRevisionFromConfig) -import App.Fossa.ReleaseGroup.Create (createMain, retrievePolicyId, retrieveTeamIds) +import App.Fossa.ReleaseGroup.Create (createMain) import Control.Algebra (Has) import Control.Carrier.Debug (ignoreDebug) import Control.Effect.FossaApiClient (FossaApiClientF (..)) -import Fossa.API.Types (ApiKey (..), ApiOpts (..), CreateReleaseGroupRequest (..), Policy (..), PolicyType (LICENSING, QUALITY, SECURITY), Team (..), defaultApiPollDelay) +import Fossa.API.CoreTypes (CreateReleaseGroupRequest (..), Policy (..), PolicyType (QUALITY, SECURITY), Team (..)) +import Fossa.API.Types (ApiKey (..), ApiOpts (..), defaultApiPollDelay) import Test.Effect (expectFatal', it', shouldBe') import Test.Fixtures qualified as Fixtures import Test.Hspec (Spec, describe) @@ -47,9 +48,6 @@ expectCreateReleaseGroupSuccess = CreateReleaseGroup createReleaseGroupReq `alwa spec :: Spec spec = do describe "Create Release Group" $ do - retrievePolicyIdSpec - retrieveTeanIdsSpec - it' "should fail when a release group with the same name already exists" $ do GetReleaseGroups `returnsOnce` [Fixtures.releaseGroup] expectFatal' $ ignoreDebug $ createMain createConfig @@ -61,45 +59,3 @@ spec = do expectCreateReleaseGroupSuccess res <- ignoreDebug $ createMain createConfig res `shouldBe'` () - -retrievePolicyIdSpec :: Spec -retrievePolicyIdSpec = do - describe "retrievePolicyId" $ do - it' "should fail when the policy title does not exist" $ do - let policies = [Fixtures.policy] - expectFatal' $ retrievePolicyId (Just "non-existent-policy-name") LICENSING policies - - it' "should fail when the policy type does not match" $ do - let policies = [Fixtures.policy] - expectFatal' $ retrievePolicyId (Just "example-policy") SECURITY policies - - it' "should fail when there are multiple policies with the same name" $ do - let policies = [Fixtures.policy, Fixtures.policy] - expectFatal' $ retrievePolicyId (Just "example-policy") SECURITY policies - - it' "should return Nothing" $ do - let policies = [Fixtures.policy] - res <- retrievePolicyId Nothing LICENSING policies - res `shouldBe'` Nothing - - it' "should successfully retrieve policy id" $ do - let policies = [Fixtures.policy] - res <- retrievePolicyId (Just "example-policy") LICENSING policies - res `shouldBe'` Just 7 - -retrieveTeanIdsSpec :: Spec -retrieveTeanIdsSpec = do - describe "retrieveTeamIds" $ do - it' "should fail if not all team names exist in org" $ do - let teams = [Fixtures.team, Fixtures.team{teamId = 11, teamName = "example-team-2"}] - expectFatal' $ retrieveTeamIds (Just ["example-team-2", "non-existent-team"]) teams - - it' "should successfully retrieve all team ids" $ do - let teams = [Fixtures.team, Fixtures.team{teamId = 11, teamName = "example-team-2"}] - res <- retrieveTeamIds Nothing teams - res `shouldBe'` Nothing - - it' "should successfully retrieve all team ids" $ do - let teams = [Fixtures.team, Fixtures.team{teamId = 11, teamName = "example-team-2"}] - res <- retrieveTeamIds (Just ["example-team", "example-team-2"]) teams - res `shouldBe'` Just [10, 11] diff --git a/test/App/Fossa/ReleaseGroup/DeleteReleaseSpec.hs b/test/App/Fossa/ReleaseGroup/DeleteReleaseSpec.hs index 78e60f7d1c..400ba1f485 100644 --- a/test/App/Fossa/ReleaseGroup/DeleteReleaseSpec.hs +++ b/test/App/Fossa/ReleaseGroup/DeleteReleaseSpec.hs @@ -8,7 +8,7 @@ import App.Fossa.ReleaseGroup.DeleteRelease (deleteReleaseMain) import Control.Algebra (Has) import Control.Carrier.Debug (ignoreDebug) import Control.Effect.FossaApiClient (FossaApiClientF (..)) -import Fossa.API.Types qualified as Types +import Fossa.API.CoreTypes qualified as Types import Test.Effect (expectFatal', it', shouldBe') import Test.Fixtures qualified as Fixtures import Test.Hspec (Spec, describe) diff --git a/test/App/Fossa/ReleaseGroup/DeleteSpec.hs b/test/App/Fossa/ReleaseGroup/DeleteSpec.hs index c1c37db0a8..165999cc2d 100644 --- a/test/App/Fossa/ReleaseGroup/DeleteSpec.hs +++ b/test/App/Fossa/ReleaseGroup/DeleteSpec.hs @@ -6,7 +6,7 @@ import App.Fossa.ReleaseGroup.Delete (deleteMain) import Control.Algebra (Has) import Control.Carrier.Debug (ignoreDebug) import Control.Effect.FossaApiClient (FossaApiClientF (..)) -import Fossa.API.Types (ReleaseGroup (releaseGroupTitle)) +import Fossa.API.CoreTypes qualified as Types import Test.Effect (expectFatal', it', shouldBe') import Test.Fixtures qualified as Fixtures import Test.Hspec (Spec, describe) @@ -25,7 +25,7 @@ spec :: Spec spec = do describe "Delete Release Group" $ do it' "should fail when the release group to delete does not exist when deleting a release group" $ do - GetReleaseGroups `returnsOnce` [Fixtures.releaseGroup{Fossa.API.Types.releaseGroupTitle = "example-title-2"}] + GetReleaseGroups `returnsOnce` [Fixtures.releaseGroup{Types.releaseGroupTitle = "example-title-2"}] expectFatal' $ ignoreDebug $ deleteMain deleteConfig it' "should fail to delete release group" $ do diff --git a/test/Test/Fixtures.hs b/test/Test/Fixtures.hs index c7b8f7a8a3..e58c33fafa 100644 --- a/test/Test/Fixtures.hs +++ b/test/Test/Fixtures.hs @@ -82,7 +82,8 @@ import Data.Text.Encoding qualified as TL import Data.Text.Extra (showT) import Discovery.Filters (AllFilters, MavenScopeFilters (MavenScopeIncludeFilters)) import Effect.Logger (Severity (..)) -import Fossa.API.Types (Archive (..), PolicyType (..)) +import Fossa.API.CoreTypes qualified as CoreAPI +import Fossa.API.Types (Archive (..)) import Fossa.API.Types qualified as API import Path (Abs, Dir, Path, mkAbsDir, mkRelDir, parseAbsDir, ()) import Srclib.Types (LicenseScanType (..), LicenseSourceUnit (..), Locator (..), SourceUnit (..), SourceUnitBuild (..), SourceUnitDependency (..), emptyLicenseUnit) @@ -135,46 +136,46 @@ invalidCreateTeamProjectsForReleaseGroupPermission = API.CustomBuildUploadPermis validCustomUploadPermissions :: API.CustomBuildUploadPermissions validCustomUploadPermissions = API.CustomBuildUploadPermissions API.ValidProjectPermission $ Just API.ValidReleaseGroupPermission -createReleaseGroupResponse :: API.CreateReleaseGroupResponse -createReleaseGroupResponse = API.CreateReleaseGroupResponse 1 +createReleaseGroupResponse :: CoreAPI.CreateReleaseGroupResponse +createReleaseGroupResponse = CoreAPI.CreateReleaseGroupResponse 1 -releaseGroup :: API.ReleaseGroup +releaseGroup :: CoreAPI.ReleaseGroup releaseGroup = - API.ReleaseGroup - { API.releaseGroupId = 1 - , API.releaseGroupTitle = "example-title" - , API.releaseGroupReleases = [release] + CoreAPI.ReleaseGroup + { CoreAPI.releaseGroupId = 1 + , CoreAPI.releaseGroupTitle = "example-title" + , CoreAPI.releaseGroupReleases = [release] } -release :: API.ReleaseGroupRelease +release :: CoreAPI.ReleaseGroupRelease release = - API.ReleaseGroupRelease - { API.releaseGroupReleaseId = 2 - , API.releaseGroupReleaseTitle = "example-release-title" - , API.releaseGroupReleaseProjects = [releaseProject] + CoreAPI.ReleaseGroupRelease + { CoreAPI.releaseGroupReleaseId = 2 + , CoreAPI.releaseGroupReleaseTitle = "example-release-title" + , CoreAPI.releaseGroupReleaseProjects = [releaseProject] } -releaseProject :: API.ReleaseProject +releaseProject :: CoreAPI.ReleaseProject releaseProject = - API.ReleaseProject - { API.releaseProjectLocator = "custom+1/example" - , API.releaseProjectRevisionId = "custom+1/example$123" - , API.releaseProjectBranch = "main" + CoreAPI.ReleaseProject + { CoreAPI.releaseProjectLocator = "custom+1/example" + , CoreAPI.releaseProjectRevisionId = "custom+1/example$123" + , CoreAPI.releaseProjectBranch = "main" } -policy :: API.Policy +policy :: CoreAPI.Policy policy = - API.Policy - { API.policyId = 7 - , API.policyTitle = "example-policy" - , API.policyType = LICENSING + CoreAPI.Policy + { CoreAPI.policyId = 7 + , CoreAPI.policyTitle = "example-policy" + , CoreAPI.policyType = CoreAPI.LICENSING } -team :: API.Team +team :: CoreAPI.Team team = - API.Team - { API.teamId = 10 - , API.teamName = "example-team" + CoreAPI.Team + { CoreAPI.teamId = 10 + , CoreAPI.teamName = "example-team" } project :: API.Project diff --git a/test/Test/MockApi.hs b/test/Test/MockApi.hs index 77d3033027..f6ce7209ce 100644 --- a/test/Test/MockApi.hs +++ b/test/Test/MockApi.hs @@ -245,6 +245,11 @@ matchExpectation a@(GetReleaseGroups{}) (ApiExpectation _ requestExpectation b@( matchExpectation a@(GetReleaseGroupReleases{}) (ApiExpectation _ requestExpectation b@(GetReleaseGroupReleases{}) resp) = checkResult requestExpectation a b resp matchExpectation a@(GetPolicies{}) (ApiExpectation _ requestExpectation b@(GetPolicies{}) resp) = checkResult requestExpectation a b resp matchExpectation a@(GetTeams{}) (ApiExpectation _ requestExpectation b@(GetTeams{}) resp) = checkResult requestExpectation a b resp +matchExpectation a@(GetProjectV2{}) (ApiExpectation _ requestExpectation b@(GetProjectV2{}) resp) = checkResult requestExpectation a b resp +matchExpectation a@(UpdateProject{}) (ApiExpectation _ requestExpectation b@(UpdateProject{}) resp) = checkResult requestExpectation a b resp +matchExpectation a@(UpdateRevision{}) (ApiExpectation _ requestExpectation b@(UpdateRevision{}) resp) = checkResult requestExpectation a b resp +matchExpectation a@(GetOrgLabels{}) (ApiExpectation _ requestExpectation b@(GetOrgLabels{}) resp) = checkResult requestExpectation a b resp +matchExpectation a@(AddTeamProjects{}) (ApiExpectation _ requestExpectation b@(AddTeamProjects{}) resp) = checkResult requestExpectation a b resp matchExpectation _ _ = Nothing -- | Handles a request in the context of the mock API.