diff --git a/docs/README.md b/docs/README.md index 115a918..15af1bb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,6 +27,10 @@ This is the complete list of the available commands provided by the CLI. | [configcat product create](configcat-product-create.md) | Create a new Product in a specified Organization identified by the `--organization-id` option | | [configcat product rm](configcat-product-rm.md) | Remove a Product identified by the `--product-id` option | | [configcat product update](configcat-product-update.md) | Update a Product identified by the `--product-id` option | +| [configcat product preferences](configcat-product-preferences.md) | Manage Product preferences | +| [configcat product preferences show](configcat-product-preferences-show.md) | Show a Product's preferences | +| [configcat product preferences update](configcat-product-preferences-update.md) | Update a Product's preferences | +| [configcat product preferences update env](configcat-product-preferences-update-env.md) | Update per-environment required reason | ### configcat config | Command | Description | | ------ | ----------- | @@ -35,6 +39,18 @@ This is the complete list of the available commands provided by the CLI. | [configcat config create](configcat-config-create.md) | Create a new Config in a specified Product identified by the `--product-id` option | | [configcat config rm](configcat-config-rm.md) | Remove a Config identified by the `--config-id` option | | [configcat config update](configcat-config-update.md) | Update a Config identified by the `--config-id` option | +### configcat webhook +| Command | Description | +| ------ | ----------- | +| [configcat webhook](configcat-webhook.md) | Manage Webhooks | +| [configcat webhook ls](configcat-webhook-ls.md) | List all Webhooks that belongs to the configured user | +| [configcat webhook show](configcat-webhook-show.md) | Print a Webhook identified by the `--webhook-id` option | +| [configcat webhook create](configcat-webhook-create.md) | Create a new Webhook | +| [configcat webhook rm](configcat-webhook-rm.md) | Remove a Webhook identified by the `--webhook-id` option | +| [configcat webhook update](configcat-webhook-update.md) | Update a Webhook identified by the `--webhook-id` option | +| [configcat webhook headers](configcat-webhook-headers.md) | Manage Webhook headers | +| [configcat webhook headers add](configcat-webhook-headers-add.md) | Add new header | +| [configcat webhook headers rm](configcat-webhook-headers-rm.md) | Remove header | ### configcat environment | Command | Description | | ------ | ----------- | @@ -119,6 +135,8 @@ This is the complete list of the available commands provided by the CLI. | Command | Description | | ------ | ----------- | | [configcat member](configcat-member.md) | Manage Members | +| [configcat member lsio](configcat-member-lsio.md) | List all pending Invitations that belongs to an Organization | +| [configcat member lsip](configcat-member-lsip.md) | List all pending Invitations that belongs to a Product | | [configcat member lso](configcat-member-lso.md) | List all Members that belongs to an Organization | | [configcat member lsp](configcat-member-lsp.md) | List all Members that belongs to a Product | | [configcat member rm](configcat-member-rm.md) | Remove Member from an Organization | diff --git a/docs/configcat-member-lsio.md b/docs/configcat-member-lsio.md new file mode 100644 index 0000000..4b268dc --- /dev/null +++ b/docs/configcat-member-lsio.md @@ -0,0 +1,22 @@ +# configcat member lsio +List all pending Invitations that belongs to an Organization +## Usage +``` +configcat member lsio [options] +``` +## Example +``` +configcat member lsio -o +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--organization-id`, `-o` | Show only an Organization's Members | +| `--json` | Format the output in JSON | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat member](configcat-member.md) | Manage Members | diff --git a/docs/configcat-member-lsip.md b/docs/configcat-member-lsip.md new file mode 100644 index 0000000..dbf3b98 --- /dev/null +++ b/docs/configcat-member-lsip.md @@ -0,0 +1,22 @@ +# configcat member lsip +List all pending Invitations that belongs to a Product +## Usage +``` +configcat member lsip [options] +``` +## Example +``` +configcat member lsip -p +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--product-id`, `-p` | Show only a Product's Members | +| `--json` | Format the output in JSON | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat member](configcat-member.md) | Manage Members | diff --git a/docs/configcat-member.md b/docs/configcat-member.md index 3a7a4e1..8bedc00 100644 --- a/docs/configcat-member.md +++ b/docs/configcat-member.md @@ -19,6 +19,8 @@ configcat member [command] ## Subcommands | Command | Description | | ------ | ----------- | +| [configcat member lsio](configcat-member-lsio.md) | List all pending Invitations that belongs to an Organization | +| [configcat member lsip](configcat-member-lsip.md) | List all pending Invitations that belongs to a Product | | [configcat member lso](configcat-member-lso.md) | List all Members that belongs to an Organization | | [configcat member lsp](configcat-member-lsp.md) | List all Members that belongs to a Product | | [configcat member rm](configcat-member-rm.md) | Remove Member from an Organization | diff --git a/docs/configcat-product-preferences-show.md b/docs/configcat-product-preferences-show.md new file mode 100644 index 0000000..95a84cf --- /dev/null +++ b/docs/configcat-product-preferences-show.md @@ -0,0 +1,24 @@ +# configcat product preferences show +Show a Product's preferences +## Aliases +`sh`, `print` +## Usage +``` +configcat product preferences show [options] +``` +## Example +``` +configcat product preferences show -i +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--product-id`, `-i` | ID of the Product | +| `--json` | Format the output in JSON | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat product preferences](configcat-product-preferences.md) | Manage Product preferences | diff --git a/docs/configcat-product-preferences-update-env.md b/docs/configcat-product-preferences-update-env.md new file mode 100644 index 0000000..abcee3c --- /dev/null +++ b/docs/configcat-product-preferences-update-env.md @@ -0,0 +1,24 @@ +# configcat product preferences update env +Update per-environment required reason +## Aliases +`e` +## Usage +``` +configcat product preferences update env [options] +``` +## Example +``` +configcat product preferences update env -i -ei :true +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--product-id`, `-i` | ID of the Product | +| `--environments`, `-ei` | Format: `:`. | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat product preferences update](configcat-product-preferences-update.md) | Update a Product's preferences | diff --git a/docs/configcat-product-preferences-update.md b/docs/configcat-product-preferences-update.md new file mode 100644 index 0000000..a2d8e51 --- /dev/null +++ b/docs/configcat-product-preferences-update.md @@ -0,0 +1,31 @@ +# configcat product preferences update +Update a Product's preferences +## Aliases +`up` +## Usage +``` +configcat product preferences update [command] [options] +``` +## Example +``` +configcat product preferences update -i --reason-required true +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--product-id`, `-i` | ID of the Product | +| `--reason-required`, `-rr` | Indicates that a mandatory note is required for saving and publishing | +| `--key-gen-mode`, `-kg` | Determines the Feature Flag key generation mode

*Possible values*: `camelCase`, `kebabCase`, `lowerCase`, `pascalCase`, `upperCase` | +| `--show-variation-id`, `-vi` | Indicates whether a variation ID's must be shown on the ConfigCat Dashboard | +| `--mandatory-setting-hint`, `-msh` | Indicates whether Feature flags and Settings must have a hint | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat product preferences](configcat-product-preferences.md) | Manage Product preferences | +## Subcommands +| Command | Description | +| ------ | ----------- | +| [configcat product preferences update env](configcat-product-preferences-update-env.md) | Update per-environment required reason | diff --git a/docs/configcat-product-preferences.md b/docs/configcat-product-preferences.md new file mode 100644 index 0000000..265bb11 --- /dev/null +++ b/docs/configcat-product-preferences.md @@ -0,0 +1,23 @@ +# configcat product preferences +Manage Product preferences +## Aliases +`pr` +## Usage +``` +configcat product preferences [command] +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat product](configcat-product.md) | Manage Products | +## Subcommands +| Command | Description | +| ------ | ----------- | +| [configcat product preferences show](configcat-product-preferences-show.md) | Show a Product's preferences | +| [configcat product preferences update](configcat-product-preferences-update.md) | Update a Product's preferences | diff --git a/docs/configcat-product.md b/docs/configcat-product.md index 869a144..a62da23 100644 --- a/docs/configcat-product.md +++ b/docs/configcat-product.md @@ -23,3 +23,4 @@ configcat product [command] | [configcat product create](configcat-product-create.md) | Create a new Product in a specified Organization identified by the `--organization-id` option | | [configcat product rm](configcat-product-rm.md) | Remove a Product identified by the `--product-id` option | | [configcat product update](configcat-product-update.md) | Update a Product identified by the `--product-id` option | +| [configcat product preferences](configcat-product-preferences.md) | Manage Product preferences | diff --git a/docs/configcat-scan.md b/docs/configcat-scan.md index 7a4226e..3d92d38 100644 --- a/docs/configcat-scan.md +++ b/docs/configcat-scan.md @@ -22,6 +22,7 @@ configcat scan ./dir -c -l 5 --print | `--commit-url-template`, `-ct` | Template url used to generate VCS commit links. Available template parameters: `commitHash`. Example: https://github.com/my/repo/commit/{commitHash} | | `--runner`, `-ru` | Overrides the default `ConfigCat CLI {version}` executor label on the ConfigCat dashboard | | `--exclude-flag-keys`, `-ex` | Exclude the given Feature Flag keys from scanning. E.g.: `-ex flag1 flag2` or `-ex 'flag1,flag2'` | +| `--alias-patterns`, `-ap` | List of custom regex patterns used to search for additional aliases | | `--verbose`, `-v`, `/v` | Print detailed execution information | | `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | | `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | diff --git a/docs/configcat-webhook-create.md b/docs/configcat-webhook-create.md new file mode 100644 index 0000000..61f9028 --- /dev/null +++ b/docs/configcat-webhook-create.md @@ -0,0 +1,27 @@ +# configcat webhook create +Create a new Webhook +## Aliases +`cr` +## Usage +``` +configcat webhook create [options] +``` +## Example +``` +configcat webhook create -c -e -u "https://example.com/hook" -m get +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--config-id`, `-c` | ID of the Config | +| `--environment-id`, `-e` | ID of the Environment | +| `--url`, `-u` | The Webhook's URL | +| `--http-method`, `-m` | The Webhook's HTTP method

*Possible values*: `get`, `post` | +| `--content`, `-co` | The Webhook's HTTP body | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat webhook](configcat-webhook.md) | Manage Webhooks | diff --git a/docs/configcat-webhook-headers-add.md b/docs/configcat-webhook-headers-add.md new file mode 100644 index 0000000..916421f --- /dev/null +++ b/docs/configcat-webhook-headers-add.md @@ -0,0 +1,26 @@ +# configcat webhook headers add +Add new header +## Aliases +`a` +## Usage +``` +configcat webhook headers add [options] +``` +## Example +``` +configcat webhook headers add -i -k Authorization -val "Bearer ..." --secure +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--webhook-id`, `-i` | ID of the Webhook to update | +| `--key`, `-k` | The Webhook header's key | +| `--value`, `-val` | The Webhook header's value | +| `--secure`, `-s` | If it's true, the Webhook header's value will kept as a secret | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat webhook headers](configcat-webhook-headers.md) | Manage Webhook headers | diff --git a/docs/configcat-webhook-headers-rm.md b/docs/configcat-webhook-headers-rm.md new file mode 100644 index 0000000..d2a36c9 --- /dev/null +++ b/docs/configcat-webhook-headers-rm.md @@ -0,0 +1,22 @@ +# configcat webhook headers rm +Remove header +## Usage +``` +configcat webhook headers rm [options] +``` +## Example +``` +configcat webhook headers rm -i -k Authorization +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--webhook-id`, `-i` | ID of the Webhook to update | +| `--key`, `-k` | The Webhook header's key | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat webhook headers](configcat-webhook-headers.md) | Manage Webhook headers | diff --git a/docs/configcat-webhook-headers.md b/docs/configcat-webhook-headers.md new file mode 100644 index 0000000..ad992ed --- /dev/null +++ b/docs/configcat-webhook-headers.md @@ -0,0 +1,23 @@ +# configcat webhook headers +Manage Webhook headers +## Aliases +`he` +## Usage +``` +configcat webhook headers [command] +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat webhook](configcat-webhook.md) | Manage Webhooks | +## Subcommands +| Command | Description | +| ------ | ----------- | +| [configcat webhook headers add](configcat-webhook-headers-add.md) | Add new header | +| [configcat webhook headers rm](configcat-webhook-headers-rm.md) | Remove header | diff --git a/docs/configcat-webhook-ls.md b/docs/configcat-webhook-ls.md new file mode 100644 index 0000000..8371749 --- /dev/null +++ b/docs/configcat-webhook-ls.md @@ -0,0 +1,22 @@ +# configcat webhook ls +List all Webhooks that belongs to the configured user +## Usage +``` +configcat webhook ls [options] +``` +## Example +``` +configcat webhook ls +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--product-id`, `-p` | Show only a Product's Webhooks | +| `--json` | Format the output in JSON | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat webhook](configcat-webhook.md) | Manage Webhooks | diff --git a/docs/configcat-webhook-rm.md b/docs/configcat-webhook-rm.md new file mode 100644 index 0000000..c611821 --- /dev/null +++ b/docs/configcat-webhook-rm.md @@ -0,0 +1,21 @@ +# configcat webhook rm +Remove a Webhook identified by the `--webhook-id` option +## Usage +``` +configcat webhook rm [options] +``` +## Example +``` +configcat webhook rm -i +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--webhook-id`, `-i` | ID of the Webhook to delete | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat webhook](configcat-webhook.md) | Manage Webhooks | diff --git a/docs/configcat-webhook-show.md b/docs/configcat-webhook-show.md new file mode 100644 index 0000000..532e2e9 --- /dev/null +++ b/docs/configcat-webhook-show.md @@ -0,0 +1,23 @@ +# configcat webhook show +Print a Webhook identified by the `--webhook-id` option +## Aliases +`sh`, `print` +## Usage +``` +configcat webhook show [options] +``` +## Example +``` +configcat webhook sh -i +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--webhook-id`, `-i` | ID of the Webhook | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat webhook](configcat-webhook.md) | Manage Webhooks | diff --git a/docs/configcat-webhook-update.md b/docs/configcat-webhook-update.md new file mode 100644 index 0000000..11aa18e --- /dev/null +++ b/docs/configcat-webhook-update.md @@ -0,0 +1,26 @@ +# configcat webhook update +Update a Webhook identified by the `--webhook-id` option +## Aliases +`up` +## Usage +``` +configcat webhook update [options] +``` +## Example +``` +configcat webhook update -i -u "https://example.com/hook" -m get +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--webhook-id`, `-i` | ID of the Webhook to update | +| `--url`, `-u` | The Webhook's URL | +| `--http-method`, `-m` | The Webhook's HTTP method

*Possible values*: `get`, `post` | +| `--content`, `-co` | The Webhook's HTTP body | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat webhook](configcat-webhook.md) | Manage Webhooks | diff --git a/docs/configcat-webhook.md b/docs/configcat-webhook.md new file mode 100644 index 0000000..503f90e --- /dev/null +++ b/docs/configcat-webhook.md @@ -0,0 +1,27 @@ +# configcat webhook +Manage Webhooks +## Aliases +`wh` +## Usage +``` +configcat webhook [command] +``` +## Options +| Option | Description | +| ------ | ----------- | +| `--verbose`, `-v`, `/v` | Print detailed execution information | +| `--non-interactive`, `-ni` | Turn off progress rendering and interactive features | +| `-h`, `/h`, `--help`, `-?`, `/?` | Show help and usage information | +## Parent Command +| Command | Description | +| ------ | ----------- | +| [configcat](index.md) | This is the Command Line Tool of ConfigCat.
ConfigCat is a hosted feature flag service: https://configcat.com
For more information, see the documentation here: https://configcat.com/docs/advanced/cli | +## Subcommands +| Command | Description | +| ------ | ----------- | +| [configcat webhook ls](configcat-webhook-ls.md) | List all Webhooks that belongs to the configured user | +| [configcat webhook show](configcat-webhook-show.md) | Print a Webhook identified by the `--webhook-id` option | +| [configcat webhook create](configcat-webhook-create.md) | Create a new Webhook | +| [configcat webhook rm](configcat-webhook-rm.md) | Remove a Webhook identified by the `--webhook-id` option | +| [configcat webhook update](configcat-webhook-update.md) | Update a Webhook identified by the `--webhook-id` option | +| [configcat webhook headers](configcat-webhook-headers.md) | Manage Webhook headers | diff --git a/docs/index.md b/docs/index.md index 115a918..15af1bb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,6 +27,10 @@ This is the complete list of the available commands provided by the CLI. | [configcat product create](configcat-product-create.md) | Create a new Product in a specified Organization identified by the `--organization-id` option | | [configcat product rm](configcat-product-rm.md) | Remove a Product identified by the `--product-id` option | | [configcat product update](configcat-product-update.md) | Update a Product identified by the `--product-id` option | +| [configcat product preferences](configcat-product-preferences.md) | Manage Product preferences | +| [configcat product preferences show](configcat-product-preferences-show.md) | Show a Product's preferences | +| [configcat product preferences update](configcat-product-preferences-update.md) | Update a Product's preferences | +| [configcat product preferences update env](configcat-product-preferences-update-env.md) | Update per-environment required reason | ### configcat config | Command | Description | | ------ | ----------- | @@ -35,6 +39,18 @@ This is the complete list of the available commands provided by the CLI. | [configcat config create](configcat-config-create.md) | Create a new Config in a specified Product identified by the `--product-id` option | | [configcat config rm](configcat-config-rm.md) | Remove a Config identified by the `--config-id` option | | [configcat config update](configcat-config-update.md) | Update a Config identified by the `--config-id` option | +### configcat webhook +| Command | Description | +| ------ | ----------- | +| [configcat webhook](configcat-webhook.md) | Manage Webhooks | +| [configcat webhook ls](configcat-webhook-ls.md) | List all Webhooks that belongs to the configured user | +| [configcat webhook show](configcat-webhook-show.md) | Print a Webhook identified by the `--webhook-id` option | +| [configcat webhook create](configcat-webhook-create.md) | Create a new Webhook | +| [configcat webhook rm](configcat-webhook-rm.md) | Remove a Webhook identified by the `--webhook-id` option | +| [configcat webhook update](configcat-webhook-update.md) | Update a Webhook identified by the `--webhook-id` option | +| [configcat webhook headers](configcat-webhook-headers.md) | Manage Webhook headers | +| [configcat webhook headers add](configcat-webhook-headers-add.md) | Add new header | +| [configcat webhook headers rm](configcat-webhook-headers-rm.md) | Remove header | ### configcat environment | Command | Description | | ------ | ----------- | @@ -119,6 +135,8 @@ This is the complete list of the available commands provided by the CLI. | Command | Description | | ------ | ----------- | | [configcat member](configcat-member.md) | Manage Members | +| [configcat member lsio](configcat-member-lsio.md) | List all pending Invitations that belongs to an Organization | +| [configcat member lsip](configcat-member-lsip.md) | List all pending Invitations that belongs to a Product | | [configcat member lso](configcat-member-lso.md) | List all Members that belongs to an Organization | | [configcat member lsp](configcat-member-lsp.md) | List all Members that belongs to a Product | | [configcat member rm](configcat-member-rm.md) | Remove Member from an Organization | diff --git a/src/ConfigCat.Cli.Models/Api/MemberModel.cs b/src/ConfigCat.Cli.Models/Api/MemberModel.cs index 5f415e1..987e8c8 100644 --- a/src/ConfigCat.Cli.Models/Api/MemberModel.cs +++ b/src/ConfigCat.Cli.Models/Api/MemberModel.cs @@ -1,9 +1,13 @@ +using System; + namespace ConfigCat.Cli.Models.Api; public class OrganizationMembersModel { public MemberModel[] Admins { get; set; } + public MemberModel[] BillingManagers { get; set; } + public OrganizationMemberModel[] Members { get; set; } } @@ -26,6 +30,8 @@ public class MemberModel public string Email { get; set; } public string FullName { get; set; } + + public bool TwoFactorEnabled { get; set; } } public class ProductMemberModel : MemberModel @@ -43,4 +49,17 @@ public class InviteMemberModel public class UpdateMembersModel { public long[] PermissionGroupIds { get; set; } +} + +public class InvitationModel +{ + public string InvitationId { get; set; } + + public string Email { get; set; } + + public int PermissionGroupId { get; set; } + + public DateTime CreatedAt { get; set; } + + public bool Expired { get; set; } } \ No newline at end of file diff --git a/src/ConfigCat.Cli.Models/Api/ProductModel.cs b/src/ConfigCat.Cli.Models/Api/ProductModel.cs index 58724ad..1ebb416 100644 --- a/src/ConfigCat.Cli.Models/Api/ProductModel.cs +++ b/src/ConfigCat.Cli.Models/Api/ProductModel.cs @@ -1,4 +1,6 @@ -namespace ConfigCat.Cli.Models.Api; +using System.Collections.Generic; + +namespace ConfigCat.Cli.Models.Api; public class ProductModel { @@ -11,4 +13,26 @@ public class ProductModel public string Description { get; set; } public int Order { get; set; } -} \ No newline at end of file +} + +public class ProductPreferencesModel +{ + public bool ReasonRequired { get; set; } + + public string KeyGenerationMode { get; set; } + + public bool ShowVariationId { get; set; } + + public bool MandatorySettingHint { get; set; } + + public IEnumerable ReasonRequiredEnvironments { get; set; } +} + +public class ReasonRequiredEnvironmentModel +{ + public string EnvironmentId { get; set; } + + public bool ReasonRequired { get; set; } + + public string EnvironmentName { get; set; } +} diff --git a/src/ConfigCat.Cli.Models/Api/WebhookModel.cs b/src/ConfigCat.Cli.Models/Api/WebhookModel.cs new file mode 100644 index 0000000..cdaf8e5 --- /dev/null +++ b/src/ConfigCat.Cli.Models/Api/WebhookModel.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConfigCat.Cli.Models.Api; + +public class WebhookModel +{ + public int WebhookId { get; set; } + + public string Url { get; set; } + + public string HttpMethod { get; set; } + + public string Content { get; set; } + + public ConfigModel Config { get; set; } + + public EnvironmentModel Environment { get; set; } + + [JsonPropertyName("webHookHeaders")] + public IEnumerable WebhookHeaders { get; set; } + + public UpdateWebhookModel ToUpdateModel() => new() + { + Url = this.Url, + HttpMethod = this.HttpMethod, + Content = this.Content, + }; +} + +public class WebhookHeaderModel +{ + public string Key { get; set; } + + public string Value { get; set; } + + public bool IsSecure { get; set; } +} + +public class UpdateWebhookModel +{ + public string Url { get; set; } + + public string HttpMethod { get; set; } + + public string Content { get; set; } +} \ No newline at end of file diff --git a/src/ConfigCat.Cli.Services/Api/MemberClient.cs b/src/ConfigCat.Cli.Services/Api/MemberClient.cs index 4bc73a0..30b8d94 100644 --- a/src/ConfigCat.Cli.Services/Api/MemberClient.cs +++ b/src/ConfigCat.Cli.Services/Api/MemberClient.cs @@ -11,6 +11,10 @@ namespace ConfigCat.Cli.Services.Api; public interface IMemberClient { + Task> GetOrganizationInvitationsAsync(string organizationId, CancellationToken token); + + Task> GetProductInvitationsAsync(string productId, CancellationToken token); + Task GetOrganizationMembersAsync(string organizationId, CancellationToken token); Task> GetProductMembersAsync(string productId, CancellationToken token); @@ -31,6 +35,12 @@ public class MemberClient( HttpClient httpClient) : ApiClient(output, config, botPolicy, httpClient), IMemberClient { + public Task> GetOrganizationInvitationsAsync(string organizationId, CancellationToken token) => + this.GetAsync>(HttpMethod.Get, $"v1/organizations/{organizationId}/invitations", token); + + public Task> GetProductInvitationsAsync(string productId, CancellationToken token) => + this.GetAsync>(HttpMethod.Get, $"v1/products/{productId}/invitations", token); + public Task GetOrganizationMembersAsync(string organizationId, CancellationToken token) => this.GetAsync(HttpMethod.Get, $"v2/organizations/{organizationId}/members", token); diff --git a/src/ConfigCat.Cli.Services/Api/ProductClient.cs b/src/ConfigCat.Cli.Services/Api/ProductClient.cs index e29e485..a30aa94 100644 --- a/src/ConfigCat.Cli.Services/Api/ProductClient.cs +++ b/src/ConfigCat.Cli.Services/Api/ProductClient.cs @@ -14,10 +14,14 @@ public interface IProductClient Task> GetProductsAsync(CancellationToken token); Task GetProductAsync(string productId, CancellationToken token); + + Task GetProductPreferencesAsync(string productId, CancellationToken token); Task CreateProductAsync(string organizationId, string name, string description, CancellationToken token); Task UpdateProductAsync(string productId, string name, string description, CancellationToken token); + + Task UpdateProductPreferencesAsync(string productId, ProductPreferencesModel model, CancellationToken token); Task DeleteProductAsync(string productId, CancellationToken token); } @@ -35,9 +39,20 @@ public Task> GetProductsAsync(CancellationToken token) public Task GetProductAsync(string productId, CancellationToken token) => this.GetAsync(HttpMethod.Get, $"v1/products/{productId}", token); + public Task GetProductPreferencesAsync(string productId, CancellationToken token) => + this.GetAsync(HttpMethod.Get, $"v1/products/{productId}/preferences", token); + public Task CreateProductAsync(string organizationId, string name, string description, CancellationToken token) => this.SendAsync(HttpMethod.Post, $"v1/organizations/{organizationId}/products", new { Name = name, Description = description }, token); + public async Task UpdateProductPreferencesAsync(string productId, ProductPreferencesModel model, CancellationToken token) + { + this.Output.Write($"Updating Product preferences... "); + await this.SendAsync(HttpMethod.Post, $"v1/products/{productId}/preferences", model, token); + this.Output.WriteSuccess(); + this.Output.WriteLine(); + } + public async Task DeleteProductAsync(string productId, CancellationToken token) { this.Output.Write($"Deleting Product... "); diff --git a/src/ConfigCat.Cli.Services/Api/WebhookClient.cs b/src/ConfigCat.Cli.Services/Api/WebhookClient.cs new file mode 100644 index 0000000..0371857 --- /dev/null +++ b/src/ConfigCat.Cli.Services/Api/WebhookClient.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using ConfigCat.Cli.Models.Api; +using ConfigCat.Cli.Models.Configuration; +using ConfigCat.Cli.Services.Json; +using ConfigCat.Cli.Services.Rendering; +using Trybot; + +namespace ConfigCat.Cli.Services.Api; + +public interface IWebhookClient +{ + Task> GetWebhooksAsync(string productId, CancellationToken token); + + Task CreateWebhookAsync(string configId, string environmentId, string url, string httpMethod, string content, CancellationToken token); + + Task GetWebhookAsync(int webhookId, CancellationToken token); + + Task UpdateWebhookAsync(int webhookId, List operations, CancellationToken token); + + Task DeleteWebhookAsync(int webhookId, CancellationToken token); +} + +public class WebhookClient( + IOutput output, + CliConfig config, + IBotPolicy botPolicy, + HttpClient httpClient) + : ApiClient(output, config, botPolicy, httpClient), IWebhookClient +{ + public Task> GetWebhooksAsync(string productId, CancellationToken token) => + this.GetAsync>(HttpMethod.Get, $"v1/products/{productId}/webhooks", token); + + public Task GetWebhookAsync(int webhookId, CancellationToken token) => + this.GetAsync(HttpMethod.Get, $"v1/webhooks/{webhookId}", token); + + public Task CreateWebhookAsync(string configId, string environmentId, string url, string httpMethod, + string content, CancellationToken token) => + this.SendAsync(HttpMethod.Post, $"v1/configs/{configId}/environments/{environmentId}/webhooks", + new { Url = url, HttpMethod = httpMethod, Content = content }, token); + + public async Task UpdateWebhookAsync(int webhookId, List operations, CancellationToken token) + { + this.Output.Write($"Updating Webhook... "); + await this.SendAsync(HttpMethod.Patch, $"v1/webhooks/{webhookId}", operations, token); + this.Output.WriteSuccess(); + this.Output.WriteLine(); + } + + public async Task DeleteWebhookAsync(int webhookId, CancellationToken token) + { + this.Output.Write($"Deleting Webhook... "); + await this.SendAsync(HttpMethod.Delete, $"v1/webhooks/{webhookId}", null, token); + this.Output.WriteSuccess(); + this.Output.WriteLine(); + } +} \ No newline at end of file diff --git a/src/ConfigCat.Cli.Services/Api/WorkspaceLoader.cs b/src/ConfigCat.Cli.Services/Api/WorkspaceLoader.cs index b4dcd51..57df745 100644 --- a/src/ConfigCat.Cli.Services/Api/WorkspaceLoader.cs +++ b/src/ConfigCat.Cli.Services/Api/WorkspaceLoader.cs @@ -22,6 +22,8 @@ public interface IWorkspaceLoader Task LoadSegmentAsync(CancellationToken token); + Task LoadWebhookAsync(CancellationToken token); + Task LoadEnvironmentAsync(CancellationToken token, string configId = null); Task LoadTagAsync(CancellationToken token); @@ -30,6 +32,8 @@ public interface IWorkspaceLoader Task LoadPermissionGroupAsync(CancellationToken token); + Task NeedsReasonAsync(string environmentId, CancellationToken token); + Task> LoadTagsAsync(CancellationToken token, string configId = null, List defaultTags = null, bool optional = false); } @@ -41,6 +45,7 @@ public class WorkspaceLoader( ISegmentClient segmentClient, ITagClient tagClient, IFlagClient flagClient, + IWebhookClient webhookClient, IPermissionGroupClient permissionGroupClient, IPrompt prompt, IOutput output, @@ -127,6 +132,14 @@ public async Task LoadPermissionGroupAsync(CancellationTok return selected; } + public async Task NeedsReasonAsync(string environmentId, CancellationToken token) + { + var environment = await environmentClient.GetEnvironmentAsync(environmentId, token); + var preferences = await productClient.GetProductPreferencesAsync(environment.Product.ProductId, token); + return preferences.ReasonRequired || + preferences.ReasonRequiredEnvironments.Any(e => e.EnvironmentId == environmentId && e.ReasonRequired); + } + public async Task LoadSegmentAsync(CancellationToken token) { var products = await PreloadProducts(token); @@ -143,6 +156,25 @@ public async Task LoadSegmentAsync(CancellationToken token) return await segmentClient.GetSegmentAsync(selected.SegmentId, token); } + + public async Task LoadWebhookAsync(CancellationToken token) + { + var products = await PreloadProducts(token); + var webhooks = new List(); + foreach (var product in products) + webhooks.AddRange(await webhookClient.GetWebhooksAsync(product.ProductId, token)); + + if (webhooks.Count == 0) + throw CreateInformalException("webhook", "webhook create"); + + var selected = await prompt.ChooseFromListAsync("Choose webhook", + webhooks.ToList(), + w => $"{w.HttpMethod.ToUpper()} {w.Url.TrimToLength(30)} ({w.Config.Name} / {w.Environment.Name})", token); + if (selected == null) + throw CreateHelpException("--webhook-id"); + + return await webhookClient.GetWebhookAsync(selected.WebhookId, token); + } public async Task LoadEnvironmentAsync(CancellationToken token, string configId = null) { diff --git a/src/ConfigCat.Cli.Services/Constants.cs b/src/ConfigCat.Cli.Services/Constants.cs index 29e995a..36b7dc2 100644 --- a/src/ConfigCat.Cli.Services/Constants.cs +++ b/src/ConfigCat.Cli.Services/Constants.cs @@ -11,6 +11,7 @@ public static class Constants public const string ApiHostEnvironmentVariableName = "CONFIGCAT_API_HOST"; public const string ApiUserNameEnvironmentVariableName = "CONFIGCAT_API_USER"; public const string ApiPasswordEnvironmentVariableName = "CONFIGCAT_API_PASS"; + public const string AliasPatternsEnvironmentVariableName = "CONFIGCAT_ALIAS_PATTERNS"; public static readonly string ConfigFilePath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), @@ -173,4 +174,34 @@ public static class SettingTypes public const string String = "string"; public const string Int = "int"; public const string Double = "double"; +} + +public static class KeyGenerationModes +{ + public static readonly string[] Collection = + [ + CamelCase, + LowerCase, + UpperCase, + PascalCase, + KebabCase + ]; + + public const string CamelCase = "camelCase"; + public const string LowerCase = "lowerCase"; + public const string UpperCase = "upperCase"; + public const string PascalCase = "pascalCase"; + public const string KebabCase = "kebabCase"; +} + +public static class HttpMethods +{ + public static readonly string[] Collection = + [ + Get, + Post, + ]; + + public const string Get = "get"; + public const string Post = "post"; } \ No newline at end of file diff --git a/src/ConfigCat.Cli.Services/Extensions/SystemExtensions.cs b/src/ConfigCat.Cli.Services/Extensions/SystemExtensions.cs index cdb5fb8..a61778e 100644 --- a/src/ConfigCat.Cli.Services/Extensions/SystemExtensions.cs +++ b/src/ConfigCat.Cli.Services/Extensions/SystemExtensions.cs @@ -136,4 +136,10 @@ public static bool IsNumberComparator(this string comparator) => "numberGreaterOrEquals" => true, _ => false }; + + public static string TrimToFitColumn(this string text) + => text == null ? "\"\"" : $"\"{text.TrimToLength(30)}\""; + + public static string TrimToLength(this string text, int length) + => text.Length > length ? $"{text[0..(length - 2)]}..." : text; } \ No newline at end of file diff --git a/src/ConfigCat.Cli.Services/Scan/AliasCollector.cs b/src/ConfigCat.Cli.Services/Scan/AliasCollector.cs index 9c45324..58695e4 100644 --- a/src/ConfigCat.Cli.Services/Scan/AliasCollector.cs +++ b/src/ConfigCat.Cli.Services/Scan/AliasCollector.cs @@ -10,15 +10,15 @@ using System.Text.RegularExpressions; using System.Linq; using ConfigCat.Cli.Models.Scan; -using System.Collections.Concurrent; using System.Collections.ObjectModel; namespace ConfigCat.Cli.Services.Scan; public interface IAliasCollector { - Task CollectAsync(IEnumerable flags, + Task CollectAsync(FlagModel[] flags, FileInfo fileToScan, + string[] matchPatterns, CancellationToken token); } @@ -36,7 +36,8 @@ public AliasCollector(IBotPolicy botPolicy, this.botPolicy.Configure(p => p.Timeout(t => t.After(TimeSpan.FromSeconds(10)))); } - public async Task CollectAsync(IEnumerable flags, FileInfo fileToScan, CancellationToken token) + public async Task CollectAsync(FlagModel[] flags, FileInfo fileToScan, + string[] matchPatterns, CancellationToken token) { try { @@ -63,7 +64,7 @@ public async Task CollectAsync(IEnumerable flags, Fi var match = Regex.Match(line, @"[`{'""]?([a-zA-Z_$0-9]*)[[`}'\""]?\s*(?>\:?\s*(?>[sS]tring)?\s*=?>?\s*(?>new|await)?)\s*\S*[@$]?[`'""](" + keys + ")[`'\"]", RegexOptions.Compiled); - + while (match.Success && !cancellation.IsCancellationRequested) { var key = match.Groups[2].Value; @@ -78,6 +79,31 @@ public async Task CollectAsync(IEnumerable flags, Fi match = match.NextMatch(); } + + if (matchPatterns.Length != 0) + { + foreach (var matchPattern in matchPatterns) + { + if (!matchPattern.Contains("CC_KEY")) + continue; + + var regMatch = Regex.Match(line, matchPattern.Replace("CC_KEY", $"[`'\"]?({keys})[`'\"]?"), RegexOptions.Compiled); + while (regMatch.Success && !cancellation.IsCancellationRequested) + { + var key = regMatch.Groups[2].Value; + var found = regMatch.Groups[1].Value; + var flag = flags.FirstOrDefault(f => f.Key == key); + + if (flag != null) + result.FoundFlags.Add(flag); + + if (flag != null && !found.IsEmpty()) + result.FlagAliases.AddOrUpdate(flag, [found], (k, v) => { v.Add(found); return v; }); + + regMatch = regMatch.NextMatch(); + } + } + } }); this.output.Verbose($"{fileToScan.FullName} search completed.", ConsoleColor.Green); diff --git a/src/ConfigCat.Cli.Services/Scan/FileScanner.cs b/src/ConfigCat.Cli.Services/Scan/FileScanner.cs index 54388a2..e14bd2a 100644 --- a/src/ConfigCat.Cli.Services/Scan/FileScanner.cs +++ b/src/ConfigCat.Cli.Services/Scan/FileScanner.cs @@ -13,8 +13,9 @@ namespace ConfigCat.Cli.Services.Scan; public interface IFileScanner { - Task> ScanAsync(IEnumerable flags, - IEnumerable filesToScan, + Task> ScanAsync(FlagModel[] flags, + FileInfo[] filesToScan, + string[] matchPatterns, int contextLines, CancellationToken token); } @@ -38,23 +39,29 @@ public FileScanner(IReferenceCollector referenceCollector, this.botPolicy.Configure(p => p.Timeout(t => t.After(TimeSpan.FromSeconds(600)))); } - public async Task> ScanAsync(IEnumerable flags, - IEnumerable filesToScan, + public async Task> ScanAsync(FlagModel[] flags, + FileInfo[] filesToScan, + string[] matchPatterns, int contextLines, CancellationToken token) { using var spinner = this.output.CreateSpinner(token); - return await this.botPolicy.ExecuteAsync(async (ctx, cancellation) => { this.output.Verbose($"Searching for flag ALIASES...", ConsoleColor.Magenta); + if (matchPatterns.Length > 0) + this.output.Verbose($"Using the following custom alias patterns: {string.Join(", ", matchPatterns.Select(p => $"'{p}'"))}"); var aliasTasks = filesToScan.TakeWhile(file => !cancellation.IsCancellationRequested) - .Select(file => this.aliasCollector.CollectAsync(flags, file, token)); + .Select(file => this.aliasCollector.CollectAsync(flags, file, matchPatterns, token)); var aliasResults = (await Task.WhenAll(aliasTasks)).Where(r => r is not null).ToArray(); foreach (var (key, value) in aliasResults.SelectMany(a => a.FlagAliases)) - key.Aliases = value.Distinct().ToList(); + { + key.Aliases ??= []; + key.Aliases.AddRange(value); + key.Aliases = key.Aliases.Distinct().ToList(); + } var foundFlags = aliasResults.SelectMany(a => a.FoundFlags).Distinct().ToArray(); diff --git a/src/ConfigCat.Cli.Services/Scan/ReferenceCollector.cs b/src/ConfigCat.Cli.Services/Scan/ReferenceCollector.cs index e00ca0c..69542b4 100644 --- a/src/ConfigCat.Cli.Services/Scan/ReferenceCollector.cs +++ b/src/ConfigCat.Cli.Services/Scan/ReferenceCollector.cs @@ -60,27 +60,36 @@ public async Task CollectAsync(IEnumerable flags using var reader = new StreamReader(stream); while (!reader.EndOfStream && !cancellation.IsCancellationRequested) { - var line = await reader.ReadLineAsync(); - if (line.Length > Constants.MaxCharCountPerLine) + var line = await reader.ReadLineAsync(cancellation); + if (line is not null) { - this.output.Verbose($"{file.FullName} contains a line that has more than {Constants.MaxCharCountPerLine} characters, skipping.", ConsoleColor.Yellow); - return null; - } - - foreach (var flagSample in flagSamples) - { - if (flagSample.KeySamples.Any(k => line.Contains(k)) || flagSample.Flag.Aliases.Any(a => line.Contains(a))) + if (line.Length > Constants.MaxCharCountPerLine) { - lineTracker.TrackReference(flagSample.Flag, line, lineNumber); + this.output.Verbose( + $"{file.FullName} contains a line that has more than {Constants.MaxCharCountPerLine} characters, skipping.", + ConsoleColor.Yellow); + lineTracker.AddLine("", lineNumber); + lineNumber++; continue; } - foreach (var sample in flagSample.Samples) + foreach (var flagSample in flagSamples) { - if (line.Contains(sample, StringComparison.OrdinalIgnoreCase)) + if (flagSample.KeySamples.Any(k => line.Contains(k)) || + flagSample.Flag.Aliases.Any(a => line.Contains(a))) + { + lineTracker.TrackReference(flagSample.Flag, line, lineNumber); + continue; + } + + foreach (var sample in flagSample.Samples) { - var originalFromLine = line.IndexOf(sample, StringComparison.OrdinalIgnoreCase); - lineTracker.TrackReference(flagSample.Flag, line, lineNumber, line.Substring(originalFromLine, sample.Length).Remove(Prefixes)); + if (line.Contains(sample, StringComparison.OrdinalIgnoreCase)) + { + var originalFromLine = line.IndexOf(sample, StringComparison.OrdinalIgnoreCase); + lineTracker.TrackReference(flagSample.Flag, line, lineNumber, + line.Substring(originalFromLine, sample.Length).Remove(Prefixes)); + } } } } diff --git a/src/ConfigCat.Cli/CommandBuilder.cs b/src/ConfigCat.Cli/CommandBuilder.cs index e43f3a8..af37156 100644 --- a/src/ConfigCat.Cli/CommandBuilder.cs +++ b/src/ConfigCat.Cli/CommandBuilder.cs @@ -10,7 +10,6 @@ using ConfigCat.Cli.Commands.PermissionGroups; using ConfigCat.Cli.Commands.ConfigJson; using ConfigCat.Cli.Commands.Flags.V2; -using Workspace = ConfigCat.Cli.Models.Configuration.Workspace; namespace ConfigCat.Cli; @@ -43,6 +42,7 @@ private static CommandDescriptor BuildDescriptors() => BuildListAllCommand(), BuildProductCommand(), BuildConfigCommand(), + BuildWebhookCommand(), BuildEnvironmentCommand(), BuildFlagCommand(), BuildFlagV2Command(), @@ -124,6 +124,48 @@ private static CommandDescriptor BuildProductCommand() => new Option(["--description", "-d"], "The updated description"), } }, + new CommandDescriptor("preferences", "Manage Product preferences") + { + Aliases = ["pr"], + SubCommands = [ + new CommandDescriptor("show", "Show a Product's preferences", "configcat product preferences show -i ") + { + Aliases = ["sh", "print"], + Handler = CreateHandler(nameof(Product.ShowProductPreferencesAsync)), + Options = [ + + new Option(["--product-id", "-i"], "ID of the Product"), + new Option(["--json"], "Format the output in JSON"), + ] + }, + new CommandDescriptor("update", "Update a Product's preferences", "configcat product preferences update -i --reason-required true") + { + Aliases = ["up"], + Handler = CreateHandler(nameof(Product.UpdateProductPreferencesAsync)), + Options = [ + + new Option(["--product-id", "-i"], "ID of the Product"), + new Option(["--reason-required", "-rr"], "Indicates that a mandatory note is required for saving and publishing"), + new Option(["--key-gen-mode", "-kg"], "Determines the Feature Flag key generation mode") + .AddSuggestions(KeyGenerationModes.Collection), + new Option(["--show-variation-id", "-vi"], "Indicates whether a variation ID's must be shown on the ConfigCat Dashboard"), + new Option(["--mandatory-setting-hint", "-msh"], "Indicates whether Feature flags and Settings must have a hint"), + ], + SubCommands = [ + new CommandDescriptor("env", "Update per-environment required reason", "configcat product preferences update env -i -ei :true") + { + Aliases = ["e"], + Handler = CreateHandler(nameof(Product.UpdateEnvSpecProductPreferencesAsync)), + Options = [ + + new Option(["--product-id", "-i"], "ID of the Product"), + new ReasonRequiredEnvironmentOption(), + ], + } + ] + } + ] + } }, }; @@ -133,6 +175,24 @@ private static CommandDescriptor BuildMemberCommand() => Aliases = new[] { "m" }, SubCommands = new[] { + new CommandDescriptor("lsio", "List all pending Invitations that belongs to an Organization", "configcat member lsio -o ") + { + Handler = CreateHandler(nameof(Member.ListOrganizationInvitationsAsync)), + Options = new Option[] + { + new Option(["--organization-id", "-o"], "Show only an Organization's Members"), + new Option(["--json"], "Format the output in JSON"), + } + }, + new CommandDescriptor("lsip", "List all pending Invitations that belongs to a Product", "configcat member lsip -p ") + { + Handler = CreateHandler(nameof(Member.ListProductInvitationsAsync)), + Options = new Option[] + { + new Option(["--product-id", "-p"], "Show only a Product's Members"), + new Option(["--json"], "Format the output in JSON"), + } + }, new CommandDescriptor("lso", "List all Members that belongs to an Organization", "configcat member lso -o ") { Handler = CreateHandler(nameof(Member.ListOrganizationMembersAsync)), @@ -251,6 +311,93 @@ private static CommandDescriptor BuildConfigCommand() => }, }, }; + + private static CommandDescriptor BuildWebhookCommand() => + new("webhook", "Manage Webhooks") + { + Aliases = new[] { "wh" }, + SubCommands = new[] + { + new CommandDescriptor("ls", "List all Webhooks that belongs to the configured user", "configcat webhook ls") + { + Options = new Option[] + { + new Option(["--product-id", "-p"], "Show only a Product's Webhooks"), + new Option(["--json"], "Format the output in JSON"), + }, + Handler = CreateHandler(nameof(Webhook.ListAllWebhooksAsync)) + }, + new CommandDescriptor("show", "Print a Webhook identified by the `--webhook-id` option", "configcat webhook sh -i ") + { + Aliases = ["sh", "print"], + Handler = CreateHandler(nameof(Webhook.ShowWebhookAsync)), + Options = new[] + { + new Option(["--webhook-id", "-i"], "ID of the Webhook"), + } + }, + new CommandDescriptor("create", "Create a new Webhook", "configcat webhook create -c -e -u \"https://example.com/hook\" -m get") + { + Aliases = new[] { "cr" }, + Handler = CreateHandler(nameof(Webhook.CreateWebhookAsync)), + Options = new[] + { + new Option(["--config-id", "-c"], "ID of the Config"), + new Option(["--environment-id", "-e"], "ID of the Environment"), + new Option(["--url", "-u"], "The Webhook's URL"), + new Option(["--http-method", "-m"], "The Webhook's HTTP method") + .AddSuggestions(HttpMethods.Collection), + new Option(["--content", "-co"], "The Webhook's HTTP body"), + } + }, + new CommandDescriptor("rm", "Remove a Webhook identified by the `--webhook-id` option", "configcat webhook rm -i ") + { + Handler = CreateHandler(nameof(Webhook.DeleteWebhookAsync)), + Options = new[] + { + new Option(["--webhook-id", "-i"], "ID of the Webhook to delete"), + } + }, + new CommandDescriptor("update", "Update a Webhook identified by the `--webhook-id` option", "configcat webhook update -i -u \"https://example.com/hook\" -m get") + { + Aliases = new[] { "up" }, + Handler = CreateHandler(nameof(Webhook.UpdateWebhookAsync)), + Options = new Option[] + { + new Option(["--webhook-id", "-i"], "ID of the Webhook to update"), + new Option(["--url", "-u"], "The Webhook's URL"), + new Option(["--http-method", "-m"], "The Webhook's HTTP method") + .AddSuggestions(HttpMethods.Collection), + new Option(["--content", "-co"], "The Webhook's HTTP body"), + } + }, + new CommandDescriptor("headers", "Manage Webhook headers") + { + Aliases = ["he"], + SubCommands = [ + new CommandDescriptor("add", "Add new header", "configcat webhook headers add -i -k Authorization -val \"Bearer ...\" --secure") + { + Aliases = ["a"], + Handler = CreateHandler(nameof(Webhook.AddWebhookHeaderAsync)), + Options = [ + new Option(["--webhook-id", "-i"], "ID of the Webhook to update"), + new Option(["--key", "-k"], "The Webhook header's key"), + new Option(["--value", "-val"], "The Webhook header's value"), + new Option(["--secure", "-s"], "If it's true, the Webhook header's value will kept as a secret"), + ] + }, + new CommandDescriptor("rm", "Remove header", "configcat webhook headers rm -i -k Authorization") + { + Handler = CreateHandler(nameof(Webhook.RemoveHeaderAsync)), + Options = [ + new Option(["--webhook-id", "-i"], "ID of the Webhook to update"), + new Option(["--key", "-k"], "The Webhook header's key"), + ] + } + ] + } + }, + }; private static CommandDescriptor BuildPermissionGroupCommand() => new("permission-group", "Manage Permission Groups") @@ -1155,6 +1302,7 @@ private static CommandDescriptor BuildScanCommand() => new Option(["--commit-url-template", "-ct"], "Template url used to generate VCS commit links. Available template parameters: `commitHash`. Example: https://github.com/my/repo/commit/{commitHash}"), new Option(["--runner", "-ru"], "Overrides the default `ConfigCat CLI {version}` executor label on the ConfigCat dashboard"), new Option(["--exclude-flag-keys", "-ex"], "Exclude the given Feature Flag keys from scanning. E.g.: `-ex flag1 flag2` or `-ex 'flag1,flag2'`"), + new Option(["--alias-patterns", "-ap"], "List of custom regex patterns used to search for additional aliases"), } }; diff --git a/src/ConfigCat.Cli/Commands/Flags/FlagPercentage.cs b/src/ConfigCat.Cli/Commands/Flags/FlagPercentage.cs index 1bd4982..d987395 100644 --- a/src/ConfigCat.Cli/Commands/Flags/FlagPercentage.cs +++ b/src/ConfigCat.Cli/Commands/Flags/FlagPercentage.cs @@ -15,6 +15,7 @@ class FlagPercentage( IFlagValueClient flagValueClient, IFlagClient flagClient, IWorkspaceLoader workspaceLoader, + IPrompt prompt, IOutput output) { public async Task UpdatePercentageRulesAsync(int? flagId, string environmentId, string reason, UpdatePercentageModel[] rules, CancellationToken token) @@ -51,6 +52,9 @@ public async Task UpdatePercentageRulesAsync(int? flagId, string environmen !(bool)result[0].Value && !(bool)result[1].Value)) throw new ShowHelpException($"Boolean percentage rules cannot have the same value."); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + value.PercentageRules = result; await flagValueClient.ReplaceValueAsync(flag.SettingId, environmentId, reason, value, token); @@ -66,6 +70,9 @@ public async Task DeletePercentageRulesAsync(int? flagId, string environmen if (environmentId.IsEmpty()) environmentId = (await workspaceLoader.LoadEnvironmentAsync(token, flag.ConfigId)).EnvironmentId; + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var value = await flagValueClient.GetValueAsync(flag.SettingId, environmentId, token); value.PercentageRules = []; await flagValueClient.ReplaceValueAsync(flag.SettingId, environmentId, reason, value, token); diff --git a/src/ConfigCat.Cli/Commands/Flags/FlagTargeting.cs b/src/ConfigCat.Cli/Commands/Flags/FlagTargeting.cs index 15b03eb..1715435 100644 --- a/src/ConfigCat.Cli/Commands/Flags/FlagTargeting.cs +++ b/src/ConfigCat.Cli/Commands/Flags/FlagTargeting.cs @@ -55,6 +55,9 @@ public async Task AddTargetingRuleAsync(int? flagId, if (!addTargetingRuleModel.FlagValue.TryParseFlagValue(flag.SettingType, out var parsed)) throw new ShowHelpException($"Flag value '{addTargetingRuleModel.FlagValue}' must respect the type '{flag.SettingType}'."); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Add($"/{FlagValueModel.TargetingRuleJsonName}/-", new TargetingModel { @@ -108,6 +111,9 @@ public async Task UpdateTargetingRuleAsync(int? flagId, if (!addTargetingRuleModel.FlagValue.TryParseFlagValue(flag.SettingType, out var parsed)) throw new ShowHelpException($"Flag value '{addTargetingRuleModel.FlagValue}' must respect the type '{flag.SettingType}'."); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Replace($"/{FlagValueModel.TargetingRuleJsonName}/{realPosition}", new TargetingModel { @@ -136,6 +142,9 @@ public async Task DeleteTargetingRuleAsync(int? flagId, string environmentI var (_, realPosition) = await this.GetRuleAsync("Choose rule to delete", flag.SettingId, environmentId, position, token); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Remove($"/{FlagValueModel.TargetingRuleJsonName}/{realPosition}"); @@ -157,6 +166,9 @@ public async Task MoveTargetingRuleAsync(int? flagId, string environmentId, var (_, realFrom) = await this.GetRuleAsync("Choose rule to re-position", flag.SettingId, environmentId, from, token); var (_, realTo) = await this.GetRuleAsync("Choose the position to move", flag.SettingId, environmentId, to, token); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Move($"/{FlagValueModel.TargetingRuleJsonName}/{realFrom}", $"/{FlagValueModel.TargetingRuleJsonName}/{realTo}"); diff --git a/src/ConfigCat.Cli/Commands/Flags/FlagValue.cs b/src/ConfigCat.Cli/Commands/Flags/FlagValue.cs index 4fa220a..9c73296 100644 --- a/src/ConfigCat.Cli/Commands/Flags/FlagValue.cs +++ b/src/ConfigCat.Cli/Commands/Flags/FlagValue.cs @@ -157,6 +157,9 @@ public async Task UpdateFlagValueAsync(int? flagId, string environmentId, s if (!flagValue.TryParseFlagValue(value.Setting.SettingType, out var parsed)) throw new ShowHelpException($"Flag value '{flagValue}' must respect the type '{value.Setting.SettingType}'."); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Replace($"/value", parsed); diff --git a/src/ConfigCat.Cli/Commands/Flags/V2/FlagPercentage.cs b/src/ConfigCat.Cli/Commands/Flags/V2/FlagPercentage.cs index 1738f2e..89ea99e 100644 --- a/src/ConfigCat.Cli/Commands/Flags/V2/FlagPercentage.cs +++ b/src/ConfigCat.Cli/Commands/Flags/V2/FlagPercentage.cs @@ -47,6 +47,9 @@ public async Task UpdatePercentageRulesAsync(int? flagId, if (value.Setting.SettingType == SettingTypes.Boolean && result.Count != 2) throw new ShowHelpException($"Boolean type can only have 2 percentage rules"); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + percentageRule.PercentageOptions = result; await flagValueClient.ReplaceValueAsync(flag.SettingId, environmentId, reason, value, token); @@ -74,6 +77,9 @@ public async Task DeletePercentageRulesAsync(int? flagId, string environmen return ExitCodes.Ok; } + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + value.TargetingRules.Remove(percentageRule); await flagValueClient.ReplaceValueAsync(flag.SettingId, environmentId, reason, value, token); @@ -94,6 +100,9 @@ public async Task UpdatePercentageAttributeAsync(int? flagId, string enviro if (attributeName.IsEmpty()) attributeName = await prompt.GetStringAsync("Percentage attribute", token, "Identifier"); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Replace($"/percentageEvaluationAttribute", attributeName); await flagValueClient.UpdateValueAsync(flag.SettingId, environmentId, reason, jsonPatchDocument.Operations, token); diff --git a/src/ConfigCat.Cli/Commands/Flags/V2/FlagTargeting.cs b/src/ConfigCat.Cli/Commands/Flags/V2/FlagTargeting.cs index 9c2267d..6b56195 100644 --- a/src/ConfigCat.Cli/Commands/Flags/V2/FlagTargeting.cs +++ b/src/ConfigCat.Cli/Commands/Flags/V2/FlagTargeting.cs @@ -53,6 +53,9 @@ public async Task AddUserTargetingRuleAsync(int? flagId, var rule = new TargetingRuleModel { Conditions = [new ConditionModel { UserCondition = condition }] }; await SetRuleThenPart(servedValue, percentageOptions, rule, flag, token); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Add($"/targetingRules/-", rule); await flagValueClient.UpdateValueAsync(flag.SettingId, environmentId, reason, jsonPatchDocument.Operations, token); @@ -95,6 +98,9 @@ public async Task AddUserConditionAsync(int? flagId, ComparisonValue = await ParseComparisonValue(comparator, comparisonValue, token) }; + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Add($"/targetingRules/{rulePosition-1}/conditions/-", new ConditionModel { UserCondition = condition }); await flagValueClient.UpdateValueAsync(flag.SettingId, environmentId, reason, jsonPatchDocument.Operations, token); @@ -140,6 +146,9 @@ public async Task AddSegmentTargetingRuleAsync(int? flagId, var rule = new TargetingRuleModel { Conditions = [new ConditionModel { SegmentCondition = condition }] }; await SetRuleThenPart(servedValue, percentageOptions, rule, flag, token); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Add($"/targetingRules/-", rule); await flagValueClient.UpdateValueAsync(flag.SettingId, environmentId, reason, jsonPatchDocument.Operations, token); @@ -183,6 +192,9 @@ public async Task AddSegmentConditionAsync(int? flagId, condition.SegmentId = segment.SegmentId; } + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Add($"/targetingRules/{rulePosition-1}/conditions/-", new ConditionModel { SegmentCondition = condition }); await flagValueClient.UpdateValueAsync(flag.SettingId, environmentId, reason, jsonPatchDocument.Operations, token); @@ -247,6 +259,9 @@ public async Task AddPrerequisiteTargetingRuleAsync(int? flagId, var rule = new TargetingRuleModel { Conditions = [new ConditionModel { PrerequisiteFlagCondition = condition }] }; await SetRuleThenPart(servedValue, percentageOptions, rule, flag, token); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Add($"/targetingRules/-", rule); await flagValueClient.UpdateValueAsync(flag.SettingId, environmentId, reason, jsonPatchDocument.Operations, token); @@ -309,6 +324,9 @@ public async Task AddPrerequisiteConditionAsync(int? flagId, condition.PrerequisiteComparisonValue = val.ToFlagValue(prerequisiteFlag.SettingType); } + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Add($"/targetingRules/{rulePosition-1}/conditions/-", new ConditionModel { PrerequisiteFlagCondition = condition }); await flagValueClient.UpdateValueAsync(flag.SettingId, environmentId, reason, jsonPatchDocument.Operations, token); @@ -333,6 +351,9 @@ public async Task DeleteRuleAsync(int? flagId, rulePosition ??= await PromptPosition("Targeting rule's position to remove", token); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Remove($"/targetingRules/{rulePosition-1}"); await flagValueClient.UpdateValueAsync(flag.SettingId, environmentId, reason, jsonPatchDocument.Operations, token); @@ -359,6 +380,9 @@ public async Task DeleteConditionAsync(int? flagId, rulePosition ??= await PromptPosition("Targeting rule's position", token); conditionPosition ??= await PromptPosition("Condition's position to remove", token); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Remove($"/targetingRules/{rulePosition-1}/conditions/{conditionPosition-1}"); await flagValueClient.UpdateValueAsync(flag.SettingId, environmentId, reason, jsonPatchDocument.Operations, token); @@ -380,6 +404,9 @@ public async Task MoveTargetingRuleAsync(int? flagId, string environmentId, from ??= await PromptPosition("Move from position", token); to ??= await PromptPosition("Move to position", token); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Move($"/targetingRules/{from-1}", $"/targetingRules/{to-1}"); @@ -412,6 +439,9 @@ public async Task UpdateRuleServedValueAsync(int? flagId, await SetRuleThenPart(servedValue, percentageOptions, rule, flag, token); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Replace($"/targetingRules/{rulePosition-1}", rule); await flagValueClient.UpdateValueAsync(flag.SettingId, environmentId, reason, jsonPatchDocument.Operations, token); diff --git a/src/ConfigCat.Cli/Commands/Flags/V2/FlagValue.cs b/src/ConfigCat.Cli/Commands/Flags/V2/FlagValue.cs index 82c5f4e..0f73fa9 100644 --- a/src/ConfigCat.Cli/Commands/Flags/V2/FlagValue.cs +++ b/src/ConfigCat.Cli/Commands/Flags/V2/FlagValue.cs @@ -246,6 +246,9 @@ public async Task UpdateFlagValueAsync(int? flagId, string environmentId, s if (!flagValue.TryParseFlagValue(value.Setting.SettingType, out var parsed)) throw new ShowHelpException($"Flag value '{flagValue}' must respect the type '{value.Setting.SettingType}'."); + if (await workspaceLoader.NeedsReasonAsync(environmentId, token) && reason.IsEmpty()) + reason = await prompt.GetStringAsync("Mandatory reason", token); + var jsonPatchDocument = new JsonPatchDocument(); jsonPatchDocument.Replace($"/defaultValue/{flag.SettingType.ToValuePropertyName()}", parsed); diff --git a/src/ConfigCat.Cli/Commands/Member.cs b/src/ConfigCat.Cli/Commands/Member.cs index 2094571..60cd01b 100644 --- a/src/ConfigCat.Cli/Commands/Member.cs +++ b/src/ConfigCat.Cli/Commands/Member.cs @@ -19,6 +19,75 @@ internal class Member( IPrompt prompt, IOutput output) { + public async Task ListOrganizationInvitationsAsync(string organizationId, bool json, CancellationToken token) + { + if (organizationId.IsEmpty()) + organizationId = (await workspaceLoader.LoadOrganizationAsync(token)).OrganizationId; + + var invitations = (await memberClient.GetOrganizationInvitationsAsync(organizationId, token)).ToList(); + + if (json) + { + output.RenderJson(invitations); + return ExitCodes.Ok; + } + + var products = await productClient.GetProductsAsync(token); + + List permissionGroups = []; + foreach (var product in products) + { + var groups = await permissionGroupClient.GetPermissionGroupsAsync(product.ProductId, token); + permissionGroups.AddRange(groups); + } + + var result = invitations.Select(m => + { + var group = permissionGroups.FirstOrDefault(g => g.PermissionGroupId == m.PermissionGroupId); + return new + { + Id = m.InvitationId, + m.Email, + m.Expired, + m.CreatedAt, + PermissionGroup = group is not null ? $"{group.Name} ({group.Product.Name})" : "-" + }; + }); + output.RenderTable(result); + return ExitCodes.Ok; + } + + public async Task ListProductInvitationsAsync(string productId, bool json, CancellationToken token) + { + if (productId.IsEmpty()) + productId = (await workspaceLoader.LoadProductAsync(token)).ProductId; + + var invitations = (await memberClient.GetProductInvitationsAsync(productId, token)).ToList(); + + if (json) + { + output.RenderJson(invitations); + return ExitCodes.Ok; + } + + var permissionGroups = await permissionGroupClient.GetPermissionGroupsAsync(productId, token); + + var result = invitations.Select(m => + { + var group = permissionGroups.FirstOrDefault(g => g.PermissionGroupId == m.PermissionGroupId); + return new + { + Id = m.InvitationId, + m.Email, + m.Expired, + m.CreatedAt, + PermissionGroup = group is not null ? $"{group.Name}" : "-" + }; + }); + output.RenderTable(result); + return ExitCodes.Ok; + } + public async Task ListOrganizationMembersAsync(string organizationId, bool json, CancellationToken token) { OrganizationMembersModel members; @@ -41,13 +110,22 @@ public async Task ListOrganizationMembersAsync(string organizationId, bool m.Email, m.FullName, m.UserId, - Permission = "Organization Admin" - }).Concat(members.Members.Select(m => new + Permission = "Organization Admin", + m.TwoFactorEnabled, + }).Concat(members.BillingManagers.Select(m => new + { + m.Email, + m.FullName, + m.UserId, + Permission = "Billing Manager", + m.TwoFactorEnabled, + })).Concat(members.Members.Select(m => new { m.Email, m.FullName, m.UserId, - Permission = string.Join(", ", m.Permissions.Select(p => $"{p.PermissionGroup.Name} ({p.Product.Name})")) + Permission = string.Join(", ", m.Permissions.Select(p => $"{p.PermissionGroup.Name} ({p.Product.Name})")), + m.TwoFactorEnabled, })); output.RenderTable(result); return ExitCodes.Ok; diff --git a/src/ConfigCat.Cli/Commands/Product.cs b/src/ConfigCat.Cli/Commands/Product.cs index 689749c..83a8e2b 100644 --- a/src/ConfigCat.Cli/Commands/Product.cs +++ b/src/ConfigCat.Cli/Commands/Product.cs @@ -2,9 +2,12 @@ using ConfigCat.Cli.Services.Api; using ConfigCat.Cli.Services.Rendering; using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using ConfigCat.Cli.Models.Api; +using ConfigCat.Cli.Services.Exceptions; namespace ConfigCat.Cli.Commands; @@ -83,4 +86,95 @@ public async Task UpdateProductAsync(string productId, string name, string await productClient.UpdateProductAsync(product.ProductId, name, description, token); return ExitCodes.Ok; } + + public async Task ShowProductPreferencesAsync(string productId, bool json, CancellationToken token) + { + var product = productId.IsEmpty() + ? await workspaceLoader.LoadProductAsync(token) + : await productClient.GetProductAsync(productId, token); + + var preferences = await productClient.GetProductPreferencesAsync(product.ProductId, token); + if (json) + { + output.RenderJson(preferences); + return ExitCodes.Ok; + } + + output.WriteDarkGray("Reason required: ").Write(preferences.ReasonRequired.ToString()).WriteLine(); + output.WriteDarkGray("Key generation mode: ").Write(preferences.KeyGenerationMode).WriteLine(); + output.WriteDarkGray("Show variation ID: ").Write(preferences.ShowVariationId.ToString()).WriteLine(); + output.WriteDarkGray("Mandatory setting hint: ").Write(preferences.MandatorySettingHint.ToString()).WriteLine(); + output.WriteDarkGray("Per-environment required reason: ").WriteLine(); + output.RenderTable(preferences.ReasonRequiredEnvironments.Select(e => new + { + Evnrionment = e.EnvironmentName, + Required = e.ReasonRequired, + })); + + return ExitCodes.Ok; + } + + public async Task UpdateProductPreferencesAsync(string productId, bool? reasonRequired, string keyGenMode, bool? showVariationId, bool? mandatorySettingHint, CancellationToken token) + { + var product = productId.IsEmpty() + ? await workspaceLoader.LoadProductAsync(token) + : await productClient.GetProductAsync(productId, token); + + var preferences = await productClient.GetProductPreferencesAsync(product.ProductId, token); + + if (!reasonRequired.HasValue && keyGenMode.IsEmpty() && !showVariationId.HasValue && + !mandatorySettingHint.HasValue) + { + reasonRequired = await prompt.ChooseFromListAsync("Reason required", ["yes", "no"], a => a, token, preferences.ReasonRequired ? "yes" : "no") == "yes"; + showVariationId = await prompt.ChooseFromListAsync("Show Variation ID", ["yes", "no"], a => a, token, preferences.ShowVariationId ? "yes" : "no") == "yes"; + mandatorySettingHint = await prompt.ChooseFromListAsync("Mandatory Setting hints", ["yes", "no"], a => a, token, preferences.MandatorySettingHint ? "yes" : "no") == "yes"; + keyGenMode = await prompt.ChooseFromListAsync("Key generation mode", KeyGenerationModes.Collection.ToList(), a => a, + token, preferences.KeyGenerationMode); + } + + if (reasonRequired.HasValue) + preferences.ReasonRequired = reasonRequired.Value; + if (!keyGenMode.IsEmpty()) + preferences.KeyGenerationMode = keyGenMode; + if (showVariationId.HasValue) + preferences.ShowVariationId = showVariationId.Value; + if (mandatorySettingHint.HasValue) + preferences.MandatorySettingHint = mandatorySettingHint.Value; + + await productClient.UpdateProductPreferencesAsync(product.ProductId, preferences, token); + + return ExitCodes.Ok; + } + + public async Task UpdateEnvSpecProductPreferencesAsync(string productId, ReasonRequiredEnvironmentModel[] environments, CancellationToken token) + { + var product = productId.IsEmpty() + ? await workspaceLoader.LoadProductAsync(token) + : await productClient.GetProductAsync(productId, token); + + var preferences = await productClient.GetProductPreferencesAsync(product.ProductId, token); + + if (environments.IsEmpty()) + { + var envModels = preferences.ReasonRequiredEnvironments.Select(ev => new + { + ev.EnvironmentId, + ev.ReasonRequired, + ev.EnvironmentName, + }).ToList(); + + var selected = await prompt.ChooseMultipleFromListAsync("Environments where reason is required", envModels, e => e.EnvironmentName, token, envModels.Where(e => e.ReasonRequired).ToList()); + environments = preferences.ReasonRequiredEnvironments.Select(e => + { + e.ReasonRequired = selected.Any(s => s.EnvironmentId == e.EnvironmentId); + return e; + }).ToArray(); + } + + preferences.ReasonRequiredEnvironments = environments; + + await productClient.UpdateProductPreferencesAsync(product.ProductId, preferences, token); + + return ExitCodes.Ok; + } } \ No newline at end of file diff --git a/src/ConfigCat.Cli/Commands/Scan.cs b/src/ConfigCat.Cli/Commands/Scan.cs index 02c30a4..54f6ced 100644 --- a/src/ConfigCat.Cli/Commands/Scan.cs +++ b/src/ConfigCat.Cli/Commands/Scan.cs @@ -42,6 +42,7 @@ public async Task InvokeAsync(DirectoryInfo directory, string fileUrlTemplate, string commitUrlTemplate, string runner, + string[] aliasPatterns, string[] excludeFlagKeys, CancellationToken token) { @@ -75,7 +76,10 @@ public async Task InvokeAsync(DirectoryInfo directory, deletedFlags = deletedFlags.Where(f => !excludeFlagKeys.Contains(f.Key)); var files = await fileCollector.CollectAsync(directory, token); - var flagReferences = await fileScanner.ScanAsync(flags.Concat(deletedFlags).ToArray(), files.ToArray(), lineCount, token); + + var patternsFromEnv = + System.Environment.GetEnvironmentVariable(Constants.AliasPatternsEnvironmentVariableName)?.Split(',') ?? []; + var flagReferences = await fileScanner.ScanAsync(flags.Concat(deletedFlags).ToArray(), files.ToArray(), patternsFromEnv.Concat(aliasPatterns).ToArray(), lineCount, token); var flagReferenceResults = flagReferences as FlagReferenceResult[] ?? flagReferences.ToArray(); var aliveFlagReferences = Filter(flagReferenceResults, r => r.FoundFlag is not DeletedFlagModel).ToArray(); diff --git a/src/ConfigCat.Cli/Commands/Webhook.cs b/src/ConfigCat.Cli/Commands/Webhook.cs new file mode 100644 index 0000000..19ce46e --- /dev/null +++ b/src/ConfigCat.Cli/Commands/Webhook.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ConfigCat.Cli.Models.Api; +using ConfigCat.Cli.Services; +using ConfigCat.Cli.Services.Api; +using ConfigCat.Cli.Services.Exceptions; +using ConfigCat.Cli.Services.Json; +using ConfigCat.Cli.Services.Rendering; + +namespace ConfigCat.Cli.Commands; + +public class Webhook( + IProductClient productClient, + IWebhookClient webhookClient, + IWorkspaceLoader workspaceLoader, + IPrompt prompt, + IOutput output) +{ + public async Task ListAllWebhooksAsync(string productId, bool json, CancellationToken token) + { + var webhooks = new List(); + if (!productId.IsEmpty()) + webhooks.AddRange(await webhookClient.GetWebhooksAsync(productId, token)); + else + { + var products = await productClient.GetProductsAsync(token); + foreach (var product in products) + webhooks.AddRange(await webhookClient.GetWebhooksAsync(product.ProductId, token)); + } + + if (json) + { + output.RenderJson(webhooks); + return ExitCodes.Ok; + } + + var itemsToRender = webhooks.Select(w => new + { + Id = w.WebhookId, + Method = w.HttpMethod, + Url = w.Url.TrimToLength(30), + Headers = string.Join(", ", w.WebhookHeaders?.Select(wh => wh.Key) ?? []), + Environment = $"{w.Environment.Name} [{w.Environment.EnvironmentId}]", + Config = $"{w.Config.Name} [{w.Config.ConfigId}]" + }); + output.RenderTable(itemsToRender); + + return ExitCodes.Ok; + } + + public async Task ShowWebhookAsync(int? webhookId, bool json, CancellationToken token) + { + var webhook = webhookId is null + ? await workspaceLoader.LoadWebhookAsync(token) + : await webhookClient.GetWebhookAsync(webhookId.Value, token); + + if (json) + { + output.RenderJson(webhook); + return ExitCodes.Ok; + } + + output.WriteDarkGray("URL: ").Write(webhook.Url).WriteLine(); + output.WriteDarkGray("HTTP method: ").Write(webhook.HttpMethod).WriteLine(); + output.WriteDarkGray("HTTP body: ").Write(webhook.Content).WriteLine(); + if (!webhook.WebhookHeaders.IsEmpty()) + { + output.WriteDarkGray("HTTP headers: ").WriteLine(); + output.RenderTable(webhook.WebhookHeaders.Select(wh => new + { + wh.Key, + Value = wh.IsSecure ? "" : wh.Value + })); + } + + return ExitCodes.Ok; + } + + public async Task CreateWebhookAsync(string configId, + string environmentId, + string url, + string httpMethod, + string content, + CancellationToken token) + { + if (configId.IsEmpty()) + configId = (await workspaceLoader.LoadConfigAsync(token)).ConfigId; + + if (environmentId.IsEmpty()) + environmentId = (await workspaceLoader.LoadEnvironmentAsync(token)).EnvironmentId; + + if (url.IsEmpty()) + url = await prompt.GetStringAsync("URL", token); + + if (content.IsEmpty()) + content = await prompt.GetStringAsync("HTTP body", token, ""); + + if (httpMethod.IsEmpty()) + httpMethod = await prompt.ChooseFromListAsync("HTTP method", HttpMethods.Collection.ToList(), a => a, token); + + if (!HttpMethods.Collection.ToList() + .Contains(httpMethod, StringComparer.OrdinalIgnoreCase)) + throw new ShowHelpException($"Http method must be one of the following: {string.Join('|', HttpMethods.Collection)}"); + + var result = await webhookClient.CreateWebhookAsync(configId, environmentId, url, httpMethod, content, token); + + output.Write(result.WebhookId.ToString()); + return ExitCodes.Ok; + } + + public async Task DeleteWebhookAsync(int? webhookId, CancellationToken token) + { + webhookId ??= (await workspaceLoader.LoadWebhookAsync(token)).WebhookId; + + await webhookClient.DeleteWebhookAsync(webhookId.Value, token); + return ExitCodes.Ok; + } + + public async Task UpdateWebhookAsync(int? webhookId, + string url, + string httpMethod, + string content, + CancellationToken token) + { + var webhook = webhookId is null + ? await workspaceLoader.LoadWebhookAsync(token) + : await webhookClient.GetWebhookAsync(webhookId.Value, token); + + if (webhookId is null) + { + if (url.IsEmpty()) + url = await prompt.GetStringAsync("URL", token, webhook.Url); + + if (httpMethod.IsEmpty()) + httpMethod = await prompt.GetStringAsync("HTTP method", token, webhook.HttpMethod); + + if (content.IsEmpty()) + content = await prompt.GetStringAsync("HTTP body", token, webhook.Content); + + if (httpMethod.IsEmpty()) + httpMethod = await prompt.ChooseFromListAsync("HTTP method", HttpMethods.Collection.ToList(), a => a, token, webhook.HttpMethod); + } + + if (url.IsEmptyOrEquals(webhook.Url) && + httpMethod.IsEmptyOrEquals(webhook.HttpMethod) && + content.IsEmptyOrEquals(webhook.Content)) + { + output.WriteNoChange(); + return ExitCodes.Ok; + } + + var patchDocument = JsonPatch.GenerateDocument(webhook.ToUpdateModel(), new UpdateWebhookModel + { + Url = url, + HttpMethod = httpMethod, + Content = content + }); + + await webhookClient.UpdateWebhookAsync(webhook.WebhookId, patchDocument.Operations, token); + return ExitCodes.Ok; + } + + public async Task AddWebhookHeaderAsync(int? webhookId, + string key, + string value, + bool secure, + CancellationToken token) + { + var webhook = webhookId is null + ? await workspaceLoader.LoadWebhookAsync(token) + : await webhookClient.GetWebhookAsync(webhookId.Value, token); + + if (key.IsEmpty()) + key = await prompt.GetStringAsync("Key", token); + + if (value.IsEmpty()) + value = await prompt.GetStringAsync("Value", token); + + var patchDocument = new JsonPatchDocument(); + patchDocument.Operations.Add(new JsonPatchOperation + { + Op = "add", + Path = "/webHookHeaders/-", + Value = new WebhookHeaderModel + { + Key = key, + Value = value, + IsSecure = secure + } + }); + + await webhookClient.UpdateWebhookAsync(webhook.WebhookId, patchDocument.Operations, token); + return ExitCodes.Ok; + } + + public async Task RemoveHeaderAsync(int? webhookId, + string key, + CancellationToken token) + { + var webhook = webhookId is null + ? await workspaceLoader.LoadWebhookAsync(token) + : await webhookClient.GetWebhookAsync(webhookId.Value, token); + + if (key.IsEmpty()) + key = await prompt.GetStringAsync("Key", token); + + var item = webhook.WebhookHeaders?.FirstOrDefault(wh => wh.Key == key); + + if (item is null) + { + output.WriteNoChange(); + return ExitCodes.Ok; + } + + var index = webhook.WebhookHeaders.ToList().IndexOf(item); + + var patchDocument = new JsonPatchDocument(); + patchDocument.Operations.Add(new JsonPatchOperation + { + Op = "remove", + Path = $"/webHookHeaders/{index}" + }); + + await webhookClient.UpdateWebhookAsync(webhook.WebhookId, patchDocument.Operations, token); + return ExitCodes.Ok; + } +} \ No newline at end of file diff --git a/src/ConfigCat.Cli/Extensions/StringExtension.cs b/src/ConfigCat.Cli/Extensions/StringExtension.cs deleted file mode 100644 index ada6ce5..0000000 --- a/src/ConfigCat.Cli/Extensions/StringExtension.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace System; - -internal static class StringExtension -{ - public static string TrimToFitColumn(this string text) - => text == null ? "\"\"" : text.Length > 30 ? $"\"{text[0..28]}...\"" : $"\"{text}\""; -} \ No newline at end of file diff --git a/src/ConfigCat.Cli/Options/ReasonRequiredEnvironmentOption.cs b/src/ConfigCat.Cli/Options/ReasonRequiredEnvironmentOption.cs new file mode 100644 index 0000000..7c41c19 --- /dev/null +++ b/src/ConfigCat.Cli/Options/ReasonRequiredEnvironmentOption.cs @@ -0,0 +1,48 @@ +using System; +using System.CommandLine; +using System.Linq; +using ConfigCat.Cli.Models.Api; + +namespace ConfigCat.Cli.Options; + +public class ReasonRequiredEnvironmentOption : Option +{ + public ReasonRequiredEnvironmentOption() : base(["--environments", "-ei"], argumentResult => + { + var length = argumentResult.Tokens.Count; + if (length == 0) + return []; + + var result = new ReasonRequiredEnvironmentModel[length]; + for (var i = 0; i < length; i++) + { + var value = argumentResult.Tokens.ElementAt(i).Value; + var indexOfSeparator = value.IndexOf(':'); + if (indexOfSeparator == -1) + { + argumentResult.ErrorMessage = $"The expression `{value}` is invalid. Required format: :"; + return null; + } + + var environmentId = value[..indexOfSeparator]; + var reasonRequired = value[(indexOfSeparator + 1)..]; + + if (!Guid.TryParse(environmentId, out _)) + { + argumentResult.ErrorMessage = $"The part of the expression `{value}` is not a valid GUID."; + return null; + } + + if (reasonRequired.IsEmpty() || !bool.TryParse(reasonRequired, out var reasonReq)) + { + argumentResult.ErrorMessage = $"The part of the expression `{value}` is not a valid boolean"; + return null; + } + + result[i] = new ReasonRequiredEnvironmentModel { EnvironmentId = environmentId, ReasonRequired = reasonReq }; + } + + return result; + }, false, "Format: `:`.") + { } +} \ No newline at end of file diff --git a/test/ConfigCat.Cli.Tests/ConfigCat.Cli.Tests.csproj b/test/ConfigCat.Cli.Tests/ConfigCat.Cli.Tests.csproj index b9f501d..74e0344 100644 --- a/test/ConfigCat.Cli.Tests/ConfigCat.Cli.Tests.csproj +++ b/test/ConfigCat.Cli.Tests/ConfigCat.Cli.Tests.csproj @@ -31,6 +31,9 @@ PreserveNewest + + PreserveNewest + diff --git a/test/ConfigCat.Cli.Tests/ScanTests.cs b/test/ConfigCat.Cli.Tests/ScanTests.cs index 991d595..262365a 100644 --- a/test/ConfigCat.Cli.Tests/ScanTests.cs +++ b/test/ConfigCat.Cli.Tests/ScanTests.cs @@ -22,7 +22,7 @@ public async Task Alias() var flag = new FlagModel { Key = "test_flag" }; var flag2 = new FlagModel { Key = "leadershipSurvey" }; - var result = await aliasCollector.CollectAsync(new[] { flag, flag2 }, new FileInfo("alias.txt"), CancellationToken.None); + var result = await aliasCollector.CollectAsync(new[] { flag, flag2 }, new FileInfo("alias.txt"), [], CancellationToken.None); var aliases = result.FlagAliases.Values.SelectMany(v => v); @@ -77,7 +77,7 @@ public async Task Scan() var flag = new FlagModel { Key = "test_flag", SettingType = "boolean" }; var file = new FileInfo("refs.txt"); - var result = await aliasCollector.CollectAsync(new[] { flag }, file, CancellationToken.None); + var result = await aliasCollector.CollectAsync(new[] { flag }, file, [], CancellationToken.None); flag.Aliases = result.FlagAliases[flag].ToList(); var references = await scanner.CollectAsync(new[] { flag }, file, 0, CancellationToken.None); @@ -216,4 +216,69 @@ public async Task Scan() Assert.Contains("wrapper::ISTESTFLAGALIASENABLED()", referenceLines); Assert.Contains("wrapper::IS_TEST_FLAG_ALIAS_ENABLED()", referenceLines); } + + [Fact] + public async Task Custom() + { + var aliasCollector = new AliasCollector(new BotPolicy(), Mock.Of()); + var scanner = new ReferenceCollector(new BotPolicy(), Mock.Of()); + + var flag = new FlagModel { Key = "test_flag", SettingType = "boolean" }; + var file = new FileInfo("custom.txt"); + + var result = await aliasCollector.CollectAsync(new[] { flag }, file, [@"(\w+) = :CC_KEY"], CancellationToken.None); + flag.Aliases = result.FlagAliases[flag].ToList(); + + Assert.Contains("CUS_TEST_FLAG", flag.Aliases); + + var references = await scanner.CollectAsync(new[] { flag }, file, 0, CancellationToken.None); + var referenceLines = references.References.Select(r => r.ReferenceLine.LineText); + + Assert.Contains("CUS_TEST_FLAG = :test_flag", referenceLines); + Assert.Contains("Somewhere else refer to CUS_TEST_FLAG", referenceLines); + } + + [Fact] + public async Task Custom_Other() + { + var aliasCollector = new AliasCollector(new BotPolicy(), Mock.Of()); + var scanner = new ReferenceCollector(new BotPolicy(), Mock.Of()); + + var flag = new FlagModel { Key = "test_flag", SettingType = "boolean" }; + var file = new FileInfo("custom.txt"); + + var result = await aliasCollector.CollectAsync(new[] { flag }, file, [@"(\w+) := FLAGS(CC_KEY)"], CancellationToken.None); + flag.Aliases = result.FlagAliases[flag].ToList(); + + Assert.Contains("is_test_flag_on", flag.Aliases); + + var references = await scanner.CollectAsync(new[] { flag }, file, 0, CancellationToken.None); + var referenceLines = references.References.Select(r => r.ReferenceLine.LineText); + + Assert.Contains("let is_test_flag_on := FLAGS('test_flag')", referenceLines); + Assert.Contains("Reference to is_test_flag_on", referenceLines); + + result = await aliasCollector.CollectAsync(new[] { flag }, file, [@"(\w+) = client_wrapper\.get_flag\(:CC_KEY\)"], CancellationToken.None); + flag.Aliases = result.FlagAliases[flag].ToList(); + + Assert.Contains("CUS2_TEST_FLAG", flag.Aliases); + + references = await scanner.CollectAsync(new[] { flag }, file, 0, CancellationToken.None); + referenceLines = references.References.Select(r => r.ReferenceLine.LineText); + + Assert.Contains("CUS2_TEST_FLAG = client_wrapper.get_flag(:test_flag)", referenceLines); + Assert.Contains("Reference to CUS2_TEST_FLAG", referenceLines); + } + + [Fact] + public async Task Alias_Patterns_Bad() + { + var aliasCollector = new AliasCollector(new BotPolicy(), Mock.Of()); + + var flag = new FlagModel { Key = "another_flag", SettingType = "boolean" }; + var file = new FileInfo("custom.txt"); + + var result = await aliasCollector.CollectAsync(new[] { flag }, file, [":CC_KEY"], CancellationToken.None); + Assert.Empty(result.FlagAliases); + } } \ No newline at end of file diff --git a/test/ConfigCat.Cli.Tests/custom.txt b/test/ConfigCat.Cli.Tests/custom.txt new file mode 100644 index 0000000..54b26f2 --- /dev/null +++ b/test/ConfigCat.Cli.Tests/custom.txt @@ -0,0 +1,15 @@ +CUS_TEST_FLAG = :test_flag + +CUS_ANOTHER_FLAG = :another_flag + +Somewhere else refer to CUS_TEST_FLAG + +CUS2_TEST_FLAG = client_wrapper.get_flag(:test_flag) + +Somewhere else refer to CUS_TEST_FLAG + +let is_test_flag_on := FLAGS('test_flag') + +Reference to is_test_flag_on + +Reference to CUS2_TEST_FLAG \ No newline at end of file diff --git a/test/integ.ps1 b/test/integ.ps1 index 77dd499..dba1a0a 100644 --- a/test/integ.ps1 +++ b/test/integ.ps1 @@ -245,6 +245,68 @@ Describe "Member tests" { } } +Describe "Webhook tests" { + BeforeAll { + $webhookId = Invoke-ConfigCat "webhook", "create", "-c", $configId, "-e", $environmentId, "-u", "https://example.com/hook", "-m", "get" + $tableResult = Invoke-ConfigCat "webhook", "ls", "-p", $productId + $tableResult | Should -Match ([regex]::Escape($webhookId)) + $tableResult | Should -Match ([regex]::Escape("https://example.com/hook")) + $tableResult | Should -Match ([regex]::Escape("get")) + } + + AfterAll { + Invoke-ConfigCat "webhook", "rm", "-i", $webhookId + } + + It "Update webhook" { + Invoke-ConfigCat "webhook", "up", "-i", $webhookId, "-u", "https://example.com/hook2", "-m", "post", "-co", "example-body" + $hookResult = Invoke-ConfigCat "webhook", "sh", "-i", $webhookId + $hookResult | Should -Match ([regex]::Escape("https://example.com/hook2")) + $hookResult | Should -Match ([regex]::Escape("post")) + $hookResult | Should -Match ([regex]::Escape("example-body")) + } + + It "Add header" { + Invoke-ConfigCat "webhook", "headers", "add", "-i", $webhookId, "-k", "Header1", "-val", "header-val" + $addHeaderResult = Invoke-ConfigCat "webhook", "sh", "-i", $webhookId + $addHeaderResult | Should -Match ([regex]::Escape("Header1")) + $addHeaderResult | Should -Match ([regex]::Escape("header-val")) + + Invoke-ConfigCat "webhook", "headers", "rm", "-i", $webhookId, "-k", "Header1" + $addHeaderResult = Invoke-ConfigCat "webhook", "sh", "-i", $webhookId + $addHeaderResult | Should -Not -Match ([regex]::Escape("Header1")) + $addHeaderResult | Should -Not -Match ([regex]::Escape("header-val")) + } + + It "Add secure header" { + Invoke-ConfigCat "webhook", "headers", "add", "-i", $webhookId, "-k", "Header2", "-val", "secure-header-val", "--secure" + $addSecureHeaderResult = Invoke-ConfigCat "webhook", "sh", "-i", $webhookId + $addSecureHeaderResult | Should -Match ([regex]::Escape("Header2")) + $addSecureHeaderResult | Should -Not -Match ([regex]::Escape("secure-header-val")) + $addSecureHeaderResult | Should -Match ([regex]::Escape("")) + } +} + +Describe "Product preferences tests" { + AfterAll { + Invoke-ConfigCat "product", "preferences", "update", "env", "-i", $productId, "-ei", "${environmentId}:false" + } + + It "Update preferences" { + Invoke-ConfigCat "product", "preferences", "update", "-i", $productId, "-rr", "true", "-kg", "pascalCase", "-vi", "true" + $prefResult = Invoke-ConfigCat "product", "preferences", "sh", "-i", $productId + $prefResult | Should -Match ([regex]::Escape("Reason required: True")) + $prefResult | Should -Match ([regex]::Escape("Key generation mode: pascalCase")) + $prefResult | Should -Match ([regex]::Escape("Show variation ID: True")) + + Invoke-ConfigCat "product", "preferences", "update", "-i", $productId, "-rr", "false" + Invoke-ConfigCat "product", "preferences", "update", "env", "-i", $productId, "-ei", "${environmentId}:true" + $prefResult = Invoke-ConfigCat "product", "preferences", "sh", "-i", $productId + $prefResult | Should -Match ([regex]::Escape("Reason required: False")) + $prefResult | Should -Match ([regex]::Escape("$newEnvironmentName True")) + } +} + Describe "Tag / Flag Tests" { BeforeAll { $tag1Id = Invoke-ConfigCat "tag", "create", "-p", $productId, "-n", "tag1", "-c", "panther" @@ -607,4 +669,9 @@ Describe "Scan Tests" { $result | Should -Not -Match ([regex]::Escape("'flag_to_scan_2'")) $result | Should -Match ([regex]::Escape("deleted feature flag/setting reference(s) found in")) } + + It "Scan custom pattern" { + $result = Invoke-ConfigCat "scan", $scanPath, "-c", $configId, "-r", "cli", "-ap", "'(\w+) = flags!(CC_KEY)'", "--print" + $result | Should -Match ([regex]::Escape("custom_alias")) + } } \ No newline at end of file diff --git a/test/sample-to-scan2.txt b/test/sample-to-scan2.txt index e452f5e..3574850 100644 --- a/test/sample-to-scan2.txt +++ b/test/sample-to-scan2.txt @@ -1,3 +1,5 @@ "flag_to_scan" -"flag_to_scan_2" \ No newline at end of file +"flag_to_scan_2" + +custom_alias = flags!(flag_to_scan) \ No newline at end of file