Browse Source

Test improvements (#900)

* Improve tests

* Fixes for comments and more tests.

* Add missing files.

* Increase timeout.

* More fixes

* More tolerant tests.

* Enable all tests.

* More fixes and comments.

* Use backup URLS.

* More consistent formatting.
pull/902/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
4879bea403
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/dev.yml
  2. 2
      .github/workflows/release.yml
  3. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  4. 17
      backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs
  5. 1
      backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs
  6. 176
      backend/src/Squidex.Web/Resources.cs
  7. 5
      backend/src/Squidex.Web/Squidex.Web.csproj
  8. 63
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  9. 6
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs
  10. 3
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs
  11. 3
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppSettingsDto.cs
  12. 3
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssetScriptsDto.cs
  13. 6
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs
  14. 3
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs
  15. 6
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs
  16. 3
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs
  17. 6
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs
  18. 3
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs
  19. 6
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs
  20. 3
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs
  21. 21
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  22. 9
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs
  23. 3
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs
  24. 9
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs
  25. 6
      backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs
  26. 3
      backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs
  27. 18
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  28. 6
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  29. 9
      backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs
  30. 24
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs
  31. 6
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs
  32. 6
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs
  33. 9
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs
  34. 27
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs
  35. 21
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs
  36. 64
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs
  37. 3
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs
  38. 3
      backend/src/Squidex/Areas/Api/Controllers/Search/Models/SearchResultDto.cs
  39. 15
      backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs
  40. 15
      backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs
  41. 3
      backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs
  42. 10
      backend/tools/TestSuite/TestSuite.ApiTests/AppClientsTests.cs
  43. 11
      backend/tools/TestSuite/TestSuite.ApiTests/AppContributorsTests.cs
  44. 15
      backend/tools/TestSuite/TestSuite.ApiTests/AppRolesTests.cs
  45. 14
      backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  46. 17
      backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs
  47. 1
      backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs
  48. 2
      backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs
  49. 2
      backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs
  50. 27
      backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs
  51. 2
      backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj

2
.github/workflows/dev.yml

@ -109,6 +109,7 @@ jobs:
with:
image: squidex/build
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
CONFIG__SERVER__URL=http://localhost:8080
WEBHOOKCATCHER__HOST__ENDPOINT=webhookcatcher
@ -122,6 +123,7 @@ jobs:
with:
image: squidex/build
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
CONFIG__SERVER__URL=http://localhost:8081/squidex
WEBHOOKCATCHER__HOST__ENDPOINT=webhookcatcher

2
.github/workflows/release.yml

@ -92,6 +92,7 @@ jobs:
with:
image: squidex/build
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
CONFIG__SERVER__URL=http://localhost:8080
WEBHOOKCATCHER__HOST__ENDPOINT=webhookcatcher
@ -105,6 +106,7 @@ jobs:
with:
image: squidex/build
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
CONFIG__SERVER__URL=http://localhost:8081/squidex
WEBHOOKCATCHER__HOST__ENDPOINT=webhookcatcher

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -257,7 +257,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
return Filter.And(
Filter.Gt(x => x.LastModified, default),
Filter.Gt(x => x.Id, DomainId.Create(string.Empty)),
Filter.Gt(x => x.IndexedAppId, appId),
Filter.Eq(x => x.IndexedAppId, appId),
Filter.Ne(x => x.IsDeleted, true),
Filter.Ne(x => x.ParentId, parentId));
}

17
backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs

@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
@ -71,10 +72,20 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
try
{
if (command is IAppCommand appCommand)
{
// Unfortunately, the commands to not share a base class.
appCommand.AppId = folderDeleted.AppId;
}
command.Actor = folderDeleted.Actor;
await commandBus.PublishAsync(command, default);
}
catch (DomainObjectNotFoundException)
{
// The asset could already be deleted by another operation.
}
catch (Exception ex)
{
log.LogError(ex, "Failed to delete asset recursively.");
@ -87,14 +98,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
foreach (var assetFolderId in childAssetFolders)
{
await PublishAsync(new DeleteAssetFolder { AppId = appId, AssetFolderId = assetFolderId });
// These deletions will create more events which will then be deleted as well.
await PublishAsync(new DeleteAssetFolder { AssetFolderId = assetFolderId });
}
var childAssets = await assetRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId, default);
foreach (var assetId in childAssets)
{
await PublishAsync(new DeleteAsset { AppId = appId, AssetId = assetId });
// Basically the leaves of the tree.
await PublishAsync(new DeleteAsset { AssetId = assetId });
}
}
}

1
backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs

@ -276,6 +276,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
try
{
// Add the current user to the app, so that the admin can see it and verify integrity.
var command = new AssignContributor
{
Actor = run.Job.Actor,

176
backend/src/Squidex.Web/Resources.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Lazy;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
@ -15,178 +14,137 @@ namespace Squidex.Web
{
public sealed class Resources
{
private readonly Dictionary<(string, string), bool> schemaPermissions = new Dictionary<(string, string), bool>();
private readonly Dictionary<(string Id, string Schema), bool> permissions = new Dictionary<(string, string), bool>();
// Contents
public bool CanReadContent(string schema) => IsAllowedForSchema(Permissions.AppContentsReadOwn, schema);
public bool CanReadContent(string schema) => Can(Permissions.AppContentsReadOwn, schema);
public bool CanCreateContent(string schema) => IsAllowedForSchema(Permissions.AppContentsCreate, schema);
public bool CanCreateContent(string schema) => Can(Permissions.AppContentsCreate, schema);
public bool CanCreateContentVersion(string schema) => IsAllowedForSchema(Permissions.AppContentsVersionCreateOwn, schema);
public bool CanCreateContentVersion(string schema) => Can(Permissions.AppContentsVersionCreateOwn, schema);
public bool CanDeleteContent(string schema) => IsAllowedForSchema(Permissions.AppContentsDeleteOwn, schema);
public bool CanDeleteContent(string schema) => Can(Permissions.AppContentsDeleteOwn, schema);
public bool CanDeleteContentVersion(string schema) => IsAllowedForSchema(Permissions.AppContentsVersionDeleteOwn, schema);
public bool CanDeleteContentVersion(string schema) => Can(Permissions.AppContentsVersionDeleteOwn, schema);
public bool CanChangeStatus(string schema) => IsAllowedForSchema(Permissions.AppContentsChangeStatus, schema);
public bool CanChangeStatus(string schema) => Can(Permissions.AppContentsChangeStatus, schema);
public bool CanCancelContentStatus(string schema) => IsAllowedForSchema(Permissions.AppContentsChangeStatusCancelOwn, schema);
public bool CanCancelContentStatus(string schema) => Can(Permissions.AppContentsChangeStatusCancelOwn, schema);
public bool CanUpdateContent(string schema) => IsAllowedForSchema(Permissions.AppContentsUpdateOwn, schema);
public bool CanUpdateContent(string schema) => Can(Permissions.AppContentsUpdateOwn, schema);
// Schemas
public bool CanUpdateSchema(string schema) => IsAllowedForSchema(Permissions.AppSchemasDelete, schema);
public bool CanUpdateSchema(string schema) => Can(Permissions.AppSchemasDelete, schema);
public bool CanUpdateSchemaScripts(string schema) => IsAllowedForSchema(Permissions.AppSchemasScripts, schema);
public bool CanUpdateSchemaScripts(string schema) => Can(Permissions.AppSchemasScripts, schema);
public bool CanPublishSchema(string schema) => IsAllowedForSchema(Permissions.AppSchemasPublish, schema);
public bool CanPublishSchema(string schema) => Can(Permissions.AppSchemasPublish, schema);
public bool CanDeleteSchema(string schema) => IsAllowedForSchema(Permissions.AppSchemasDelete, schema);
public bool CanDeleteSchema(string schema) => Can(Permissions.AppSchemasDelete, schema);
[Lazy]
public bool CanCreateSchema => IsAllowed(Permissions.AppSchemasCreate);
public bool CanCreateSchema => Can(Permissions.AppSchemasCreate);
[Lazy]
public bool CanUpdateSettings => IsAllowed(Permissions.AppUpdateSettings);
public bool CanUpdateSettings => Can(Permissions.AppUpdateSettings);
// Asset Scripts
[Lazy]
public bool CanUpdateAssetsScripts => IsAllowed(Permissions.AppAssetsScriptsUpdate);
public bool CanUpdateAssetsScripts => Can(Permissions.AppAssetsScriptsUpdate);
// Contributors
[Lazy]
public bool CanAssignContributor => IsAllowed(Permissions.AppContributorsAssign);
public bool CanAssignContributor => Can(Permissions.AppContributorsAssign);
[Lazy]
public bool CanRevokeContributor => IsAllowed(Permissions.AppContributorsRevoke);
public bool CanRevokeContributor => Can(Permissions.AppContributorsRevoke);
// Workflows
[Lazy]
public bool CanCreateWorkflow => IsAllowed(Permissions.AppWorkflowsCreate);
public bool CanCreateWorkflow => Can(Permissions.AppWorkflowsCreate);
[Lazy]
public bool CanUpdateWorkflow => IsAllowed(Permissions.AppWorkflowsUpdate);
public bool CanUpdateWorkflow => Can(Permissions.AppWorkflowsUpdate);
[Lazy]
public bool CanDeleteWorkflow => IsAllowed(Permissions.AppWorkflowsDelete);
public bool CanDeleteWorkflow => Can(Permissions.AppWorkflowsDelete);
// Roles
[Lazy]
public bool CanCreateRole => IsAllowed(Permissions.AppRolesCreate);
public bool CanCreateRole => Can(Permissions.AppRolesCreate);
[Lazy]
public bool CanUpdateRole => IsAllowed(Permissions.AppRolesUpdate);
public bool CanUpdateRole => Can(Permissions.AppRolesUpdate);
[Lazy]
public bool CanDeleteRole => IsAllowed(Permissions.AppRolesDelete);
public bool CanDeleteRole => Can(Permissions.AppRolesDelete);
// Languages
[Lazy]
public bool CanCreateLanguage => IsAllowed(Permissions.AppLanguagesCreate);
public bool CanCreateLanguage => Can(Permissions.AppLanguagesCreate);
[Lazy]
public bool CanUpdateLanguage => IsAllowed(Permissions.AppLanguagesUpdate);
public bool CanUpdateLanguage => Can(Permissions.AppLanguagesUpdate);
[Lazy]
public bool CanDeleteLanguage => IsAllowed(Permissions.AppLanguagesDelete);
public bool CanDeleteLanguage => Can(Permissions.AppLanguagesDelete);
// Clients
[Lazy]
public bool CanCreateClient => IsAllowed(Permissions.AppClientsCreate);
public bool CanCreateClient => Can(Permissions.AppClientsCreate);
[Lazy]
public bool CanUpdateClient => IsAllowed(Permissions.AppClientsUpdate);
public bool CanUpdateClient => Can(Permissions.AppClientsUpdate);
[Lazy]
public bool CanDeleteClient => IsAllowed(Permissions.AppClientsDelete);
public bool CanDeleteClient => Can(Permissions.AppClientsDelete);
// Rules
[Lazy]
public bool CanDisableRule => IsAllowed(Permissions.AppRulesDisable);
public bool CanDisableRule => Can(Permissions.AppRulesDisable);
[Lazy]
public bool CanCreateRule => IsAllowed(Permissions.AppRulesCreate);
public bool CanCreateRule => Can(Permissions.AppRulesCreate);
[Lazy]
public bool CanUpdateRule => IsAllowed(Permissions.AppRulesUpdate);
public bool CanUpdateRule => Can(Permissions.AppRulesUpdate);
[Lazy]
public bool CanDeleteRule => IsAllowed(Permissions.AppRulesDelete);
public bool CanDeleteRule => Can(Permissions.AppRulesDelete);
[Lazy]
public bool CanReadRuleEvents => IsAllowed(Permissions.AppRulesEventsRead);
public bool CanReadRuleEvents => Can(Permissions.AppRulesEventsRead);
[Lazy]
public bool CanUpdateRuleEvents => IsAllowed(Permissions.AppRulesEventsUpdate);
public bool CanUpdateRuleEvents => Can(Permissions.AppRulesEventsUpdate);
[Lazy]
public bool CanRunRuleEvents => IsAllowed(Permissions.AppRulesEventsRun);
public bool CanRunRuleEvents => Can(Permissions.AppRulesEventsRun);
[Lazy]
public bool CanDeleteRuleEvents => IsAllowed(Permissions.AppRulesEventsDelete);
public bool CanDeleteRuleEvents => Can(Permissions.AppRulesEventsDelete);
// Users
[Lazy]
public bool CanReadUsers => IsAllowed(Permissions.AdminUsersRead);
public bool CanReadUsers => Can(Permissions.AdminUsersRead);
[Lazy]
public bool CanCreateUser => IsAllowed(Permissions.AdminUsersCreate);
public bool CanCreateUser => Can(Permissions.AdminUsersCreate);
[Lazy]
public bool CanLockUser => IsAllowed(Permissions.AdminUsersLock);
public bool CanLockUser => Can(Permissions.AdminUsersLock);
[Lazy]
public bool CanUnlockUser => IsAllowed(Permissions.AdminUsersUnlock);
public bool CanUnlockUser => Can(Permissions.AdminUsersUnlock);
[Lazy]
public bool CanUpdateUser => IsAllowed(Permissions.AdminUsersUpdate);
public bool CanUpdateUser => Can(Permissions.AdminUsersUpdate);
// Assets
[Lazy]
public bool CanUploadAsset => IsAllowed(Permissions.AppAssetsUpload);
public bool CanUploadAsset => Can(Permissions.AppAssetsUpload);
[Lazy]
public bool CanCreateAsset => IsAllowed(Permissions.AppAssetsCreate);
public bool CanCreateAsset => Can(Permissions.AppAssetsCreate);
[Lazy]
public bool CanDeleteAsset => IsAllowed(Permissions.AppAssetsDelete);
public bool CanDeleteAsset => Can(Permissions.AppAssetsDelete);
[Lazy]
public bool CanUpdateAsset => IsAllowed(Permissions.AppAssetsUpdate);
public bool CanUpdateAsset => Can(Permissions.AppAssetsUpdate);
[Lazy]
public bool CanReadAssets => IsAllowed(Permissions.AppAssetsRead);
public bool CanReadAssets => Can(Permissions.AppAssetsRead);
// Events
[Lazy]
public bool CanReadEvents => IsAllowed(Permissions.AdminEventsRead);
public bool CanReadEvents => Can(Permissions.AdminEventsRead);
[Lazy]
public bool CanManageEvents => IsAllowed(Permissions.AdminEventsManage);
public bool CanManageEvents => Can(Permissions.AdminEventsManage);
// Backups
[Lazy]
public bool CanRestoreBackup => IsAllowed(Permissions.AdminRestore);
public bool CanRestoreBackup => Can(Permissions.AdminRestore);
[Lazy]
public bool CanCreateBackup => IsAllowed(Permissions.AppBackupsCreate);
public bool CanCreateBackup => Can(Permissions.AppBackupsCreate);
[Lazy]
public bool CanDeleteBackup => IsAllowed(Permissions.AppBackupsDelete);
public bool CanDeleteBackup => Can(Permissions.AppBackupsDelete);
[Lazy]
public bool CanDownloadBackup => IsAllowed(Permissions.AppBackupsDownload);
public bool CanDownloadBackup => Can(Permissions.AppBackupsDownload);
public Context Context { get; set; }
[Lazy]
public string? App => GetAppName();
[Lazy]
public string? Schema => GetAppName();
public DomainId AppId => GetAppId();
public ApiController Controller { get; }
public Context Context { get; set; }
public Resources(ApiController controller)
{
Controller = controller;
@ -219,16 +177,21 @@ namespace Squidex.Web
return Context.UserPermissions.Includes(permission) || additional?.Includes(permission) == true;
}
public bool IsAllowedForSchema(string id, string schema)
public bool Can(string id)
{
return schemaPermissions.GetOrAdd((id, schema), k => IsAllowed(k.Item1, "*", k.Item2));
return permissions.GetOrAdd((Id: id, string.Empty), k => IsAllowed(k.Id, Permission.Any, Permission.Any));
}
public bool Can(string id, string schema)
{
return permissions.GetOrAdd((Id: id, Schema: schema), k => IsAllowed(k.Id, Permission.Any, k.Schema));
}
public bool IsAllowed(string id, string app = Permission.Any, string schema = Permission.Any, PermissionSet? additional = null)
{
if (app == Permission.Any)
{
var fallback = App;
var fallback = GetAppName();
if (!string.IsNullOrWhiteSpace(fallback))
{
@ -238,7 +201,7 @@ namespace Squidex.Web
if (schema == Permission.Any)
{
var fallback = Controller.HttpContext.Features.Get<ISchemaFeature>()?.Schema.SchemaDef.Name;
var fallback = GetSchemaName();
if (!string.IsNullOrWhiteSpace(fallback))
{
@ -256,6 +219,11 @@ namespace Squidex.Web
return Controller.HttpContext.Context().App?.Name;
}
private string? GetSchemaName()
{
return Controller.HttpContext.Features.Get<ISchemaFeature>()?.Schema.SchemaDef.Name;
}
private DomainId GetAppId()
{
return Controller.HttpContext.Context().App?.Id ?? default;

5
backend/src/Squidex.Web/Squidex.Web.csproj

@ -13,14 +13,9 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fody" Version="6.6.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="GraphQL" Version="5.3.0" />
<PackageReference Include="GraphQL.NewtonsoftJson" Version="5.3.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="6.1.0" />
<PackageReference Include="Lazy.Fody" Version="1.11.0" PrivateAssets="all" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

63
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -133,104 +133,125 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
{
var values = new { app = Name };
AddGetLink("ping", resources.Url<PingController>(x => nameof(x.GetAppPing), values));
AddGetLink("ping",
resources.Url<PingController>(x => nameof(x.GetAppPing), values));
if (app.Image != null)
{
AddGetLink("image", resources.Url<AppImageController>(x => nameof(x.GetImage), values));
AddGetLink("image",
resources.Url<AppImageController>(x => nameof(x.GetImage), values));
}
if (isContributor)
{
AddDeleteLink("leave", resources.Url<AppContributorsController>(x => nameof(x.DeleteMyself), values));
AddDeleteLink("leave",
resources.Url<AppContributorsController>(x => nameof(x.DeleteMyself), values));
}
if (resources.IsAllowed(Shared.Permissions.AppDelete, Name, additional: permissions))
{
AddDeleteLink("delete", resources.Url<AppsController>(x => nameof(x.DeleteApp), values));
AddDeleteLink("delete",
resources.Url<AppsController>(x => nameof(x.DeleteApp), values));
}
if (resources.IsAllowed(Shared.Permissions.AppUpdate, Name, additional: permissions))
{
AddPutLink("update", resources.Url<AppsController>(x => nameof(x.PutApp), values));
AddPutLink("update",
resources.Url<AppsController>(x => nameof(x.PutApp), values));
}
if (resources.IsAllowed(Shared.Permissions.AppAssetsRead, Name, additional: permissions))
{
AddGetLink("assets", resources.Url<AssetsController>(x => nameof(x.GetAssets), values));
AddGetLink("assets",
resources.Url<AssetsController>(x => nameof(x.GetAssets), values));
}
if (resources.IsAllowed(Shared.Permissions.AppBackupsRead, Name, additional: permissions))
{
AddGetLink("backups", resources.Url<BackupsController>(x => nameof(x.GetBackups), values));
AddGetLink("backups",
resources.Url<BackupsController>(x => nameof(x.GetBackups), values));
}
if (resources.IsAllowed(Shared.Permissions.AppClientsRead, Name, additional: permissions))
{
AddGetLink("clients", resources.Url<AppClientsController>(x => nameof(x.GetClients), values));
AddGetLink("clients",
resources.Url<AppClientsController>(x => nameof(x.GetClients), values));
}
if (resources.IsAllowed(Shared.Permissions.AppContributorsRead, Name, additional: permissions))
{
AddGetLink("contributors", resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values));
AddGetLink("contributors",
resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values));
}
if (resources.IsAllowed(Shared.Permissions.AppLanguagesRead, Name, additional: permissions))
{
AddGetLink("languages", resources.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values));
AddGetLink("languages",
resources.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values));
}
if (resources.IsAllowed(Shared.Permissions.AppPlansRead, Name, additional: permissions))
{
AddGetLink("plans", resources.Url<AppPlansController>(x => nameof(x.GetPlans), values));
AddGetLink("plans",
resources.Url<AppPlansController>(x => nameof(x.GetPlans), values));
}
if (resources.IsAllowed(Shared.Permissions.AppRolesRead, Name, additional: permissions))
{
AddGetLink("roles", resources.Url<AppRolesController>(x => nameof(x.GetRoles), values));
AddGetLink("roles",
resources.Url<AppRolesController>(x => nameof(x.GetRoles), values));
}
if (resources.IsAllowed(Shared.Permissions.AppRulesRead, Name, additional: permissions))
{
AddGetLink("rules", resources.Url<RulesController>(x => nameof(x.GetRules), values));
AddGetLink("rules",
resources.Url<RulesController>(x => nameof(x.GetRules), values));
}
if (resources.IsAllowed(Shared.Permissions.AppSchemasRead, Name, additional: permissions))
{
AddGetLink("schemas", resources.Url<SchemasController>(x => nameof(x.GetSchemas), values));
AddGetLink("schemas",
resources.Url<SchemasController>(x => nameof(x.GetSchemas), values));
}
if (resources.IsAllowed(Shared.Permissions.AppWorkflowsRead, Name, additional: permissions))
{
AddGetLink("workflows", resources.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values));
AddGetLink("workflows",
resources.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values));
}
if (resources.IsAllowed(Shared.Permissions.AppSchemasCreate, Name, additional: permissions))
{
AddPostLink("schemas/create", resources.Url<SchemasController>(x => nameof(x.PostSchema), values));
AddPostLink("schemas/create",
resources.Url<SchemasController>(x => nameof(x.PostSchema), values));
}
if (resources.IsAllowed(Shared.Permissions.AppAssetsCreate, Name, additional: permissions))
{
AddPostLink("assets/create", resources.Url<SchemasController>(x => nameof(x.PostSchema), values));
AddPostLink("assets/create",
resources.Url<SchemasController>(x => nameof(x.PostSchema), values));
}
if (resources.IsAllowed(Shared.Permissions.AppImageUpload, Name, additional: permissions))
{
AddPostLink("image/upload", resources.Url<AppsController>(x => nameof(x.UploadImage), values));
AddPostLink("image/upload",
resources.Url<AppsController>(x => nameof(x.UploadImage), values));
}
if (resources.IsAllowed(Shared.Permissions.AppImageDelete, Name, additional: permissions))
{
AddDeleteLink("image/delete", resources.Url<AppsController>(x => nameof(x.DeleteImage), values));
AddDeleteLink("image/delete",
resources.Url<AppsController>(x => nameof(x.DeleteImage), values));
}
if (resources.IsAllowed(Shared.Permissions.AppAssetsScriptsUpdate, Name, additional: permissions))
{
AddDeleteLink("assets/scripts", resources.Url<AppAssetsController>(x => nameof(x.GetAssetScripts), values));
AddDeleteLink("assets/scripts",
resources.Url<AppAssetsController>(x => nameof(x.GetAssetScripts), values));
}
AddGetLink("settings", resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values));
AddGetLink("settings",
resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values));
return this;
}

6
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs

@ -65,12 +65,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
{
if (resources.CanUpdateLanguage)
{
AddPutLink("update", resources.Url<AppLanguagesController>(x => nameof(x.PutLanguage), values));
AddPutLink("update",
resources.Url<AppLanguagesController>(x => nameof(x.PutLanguage), values));
}
if (resources.CanDeleteLanguage && app.Languages.Languages.Count > 1)
{
AddDeleteLink("delete", resources.Url<AppLanguagesController>(x => nameof(x.DeleteLanguage), values));
AddDeleteLink("delete",
resources.Url<AppLanguagesController>(x => nameof(x.DeleteLanguage), values));
}
}

3
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs

@ -43,7 +43,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
if (resources.CanCreateLanguage)
{
AddPostLink("create", resources.Url<AppLanguagesController>(x => nameof(x.PostLanguage), values));
AddPostLink("create",
resources.Url<AppLanguagesController>(x => nameof(x.PostLanguage), values));
}
return this;

3
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppSettingsDto.cs

@ -64,7 +64,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
if (resources.CanUpdateSettings)
{
AddPutLink("update", resources.Url<AppSettingsController>(x => nameof(x.PutSettings), values));
AddPutLink("update",
resources.Url<AppSettingsController>(x => nameof(x.PutSettings), values));
}
return this;

3
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssetScriptsDto.cs

@ -58,7 +58,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
if (resources.CanUpdateAssetsScripts)
{
AddPutLink("update", resources.Url<AppAssetsController>(x => nameof(x.PutAssetScripts), values));
AddPutLink("update",
resources.Url<AppAssetsController>(x => nameof(x.PutAssetScripts), values));
}
return this;

6
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs

@ -65,12 +65,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
if (resources.CanUpdateClient)
{
AddPutLink("update", resources.Url<AppClientsController>(x => nameof(x.PutClient), values));
AddPutLink("update",
resources.Url<AppClientsController>(x => nameof(x.PutClient), values));
}
if (resources.CanDeleteClient)
{
AddDeleteLink("delete", resources.Url<AppClientsController>(x => nameof(x.DeleteClient), values));
AddDeleteLink("delete",
resources.Url<AppClientsController>(x => nameof(x.DeleteClient), values));
}
return this;

3
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs

@ -40,7 +40,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
if (resources.CanCreateClient)
{
AddPostLink("create", resources.Url<AppClientsController>(x => nameof(x.PostClient), values));
AddPostLink("create",
resources.Url<AppClientsController>(x => nameof(x.PostClient), values));
}
return this;

6
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs

@ -70,14 +70,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
{
var values = new { app };
AddPostLink("update", resources.Url<AppContributorsController>(x => nameof(x.PostContributor), values));
AddPostLink("update",
resources.Url<AppContributorsController>(x => nameof(x.PostContributor), values));
}
if (resources.CanRevokeContributor)
{
var values = new { app, id = ContributorId };
AddDeleteLink("delete", resources.Url<AppContributorsController>(x => nameof(x.DeleteContributor), values));
AddDeleteLink("delete",
resources.Url<AppContributorsController>(x => nameof(x.DeleteContributor), values));
}
}

3
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs

@ -77,7 +77,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
if (resources.CanAssignContributor && (MaxContributors < 0 || Items.Length < MaxContributors))
{
AddPostLink("create", resources.Url<AppContributorsController>(x => nameof(x.PostContributor), values));
AddPostLink("create",
resources.Url<AppContributorsController>(x => nameof(x.PostContributor), values));
}
return this;

6
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs

@ -81,12 +81,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
{
if (resources.CanUpdateRole)
{
AddPutLink("update", resources.Url<AppRolesController>(x => nameof(x.PutRole), values));
AddPutLink("update",
resources.Url<AppRolesController>(x => nameof(x.PutRole), values));
}
if (resources.CanDeleteRole && NumClients == 0 && NumContributors == 0)
{
AddDeleteLink("delete", resources.Url<AppRolesController>(x => nameof(x.DeleteRole), values));
AddDeleteLink("delete",
resources.Url<AppRolesController>(x => nameof(x.DeleteRole), values));
}
}

3
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs

@ -42,7 +42,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
if (resources.CanCreateRole)
{
AddPostLink("create", resources.Url<AppRolesController>(x => nameof(x.PostRole), values));
AddPostLink("create",
resources.Url<AppRolesController>(x => nameof(x.PostRole), values));
}
return this;

6
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs

@ -61,12 +61,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
if (resources.CanUpdateWorkflow)
{
AddPutLink("update", resources.Url<AppWorkflowsController>(x => nameof(x.PutWorkflow), values));
AddPutLink("update",
resources.Url<AppWorkflowsController>(x => nameof(x.PutWorkflow), values));
}
if (resources.CanDeleteWorkflow)
{
AddDeleteLink("delete", resources.Url<AppWorkflowsController>(x => nameof(x.DeleteWorkflow), values));
AddDeleteLink("delete",
resources.Url<AppWorkflowsController>(x => nameof(x.DeleteWorkflow), values));
}
return this;

3
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs

@ -52,7 +52,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
if (resources.CanCreateWorkflow)
{
AddPostLink("create", resources.Url<AppWorkflowsController>(x => nameof(x.PostWorkflow), values));
AddPostLink("create",
resources.Url<AppWorkflowsController>(x => nameof(x.PostWorkflow), values));
}
return this;

21
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -189,39 +189,46 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
if (resources.CanUpdateAsset)
{
AddPutLink("update", resources.Url<AssetsController>(x => nameof(x.PutAsset), values));
AddPutLink("update",
resources.Url<AssetsController>(x => nameof(x.PutAsset), values));
}
if (resources.CanUpdateAsset)
{
AddPutLink("move", resources.Url<AssetsController>(x => nameof(x.PutAssetParent), values));
AddPutLink("move",
resources.Url<AssetsController>(x => nameof(x.PutAssetParent), values));
}
if (resources.CanUploadAsset)
{
AddPutLink("upload", resources.Url<AssetsController>(x => nameof(x.PutAssetContent), values));
AddPutLink("upload",
resources.Url<AssetsController>(x => nameof(x.PutAssetContent), values));
}
if (resources.CanDeleteAsset)
{
AddDeleteLink("delete", resources.Url<AssetsController>(x => nameof(x.DeleteAsset), values));
AddDeleteLink("delete",
resources.Url<AssetsController>(x => nameof(x.DeleteAsset), values));
}
if (!string.IsNullOrWhiteSpace(Slug))
{
var idValues = new { app, idOrSlug = Id, more = Slug };
AddGetLink("content", resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), idValues));
AddGetLink("content",
resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), idValues));
var slugValues = new { app, idOrSlug = Slug };
AddGetLink("content/slug", resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), slugValues));
AddGetLink("content/slug",
resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), slugValues));
}
else
{
var idValues = new { app, idOrSlug = Id };
AddGetLink("content", resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), idValues));
AddGetLink("content",
resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), idValues));
}
return this;

9
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs

@ -49,14 +49,17 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
if (resources.CanUpdateAsset)
{
AddPutLink("update", resources.Url<AssetFoldersController>(x => nameof(x.PutAssetFolder), values));
AddPutLink("update",
resources.Url<AssetFoldersController>(x => nameof(x.PutAssetFolder), values));
AddPutLink("move", resources.Url<AssetFoldersController>(x => nameof(x.PutAssetFolderParent), values));
AddPutLink("move",
resources.Url<AssetFoldersController>(x => nameof(x.PutAssetFolderParent), values));
}
if (resources.CanUpdateAsset)
{
AddDeleteLink("delete", resources.Url<AssetFoldersController>(x => nameof(x.DeleteAssetFolder), values));
AddDeleteLink("delete",
resources.Url<AssetFoldersController>(x => nameof(x.DeleteAssetFolder), values));
}
return this;

3
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs

@ -52,7 +52,8 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
if (resources.CanUpdateAsset)
{
AddPostLink("create", resources.Url<AssetFoldersController>(x => nameof(x.PostAssetFolder), values));
AddPostLink("create",
resources.Url<AssetFoldersController>(x => nameof(x.PostAssetFolder), values));
}
return this;

9
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs

@ -44,17 +44,20 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
if (resources.CanCreateAsset)
{
AddPostLink("create", resources.Url<AssetsController>(x => nameof(x.PostAsset), values));
AddPostLink("create",
resources.Url<AssetsController>(x => nameof(x.PostAsset), values));
}
if (resources.CanUpdateAsset)
{
var tagValue = new { values.app, name = "tag" };
AddPutLink("tags/rename", resources.Url<AssetsController>(x => nameof(x.PutTag), tagValue));
AddPutLink("tags/rename",
resources.Url<AssetsController>(x => nameof(x.PutTag), tagValue));
}
AddGetLink("tags", resources.Url<AssetsController>(x => nameof(x.GetTags), values));
AddGetLink("tags",
resources.Url<AssetsController>(x => nameof(x.GetTags), values));
return this;
}

6
backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs

@ -58,14 +58,16 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models
{
var values = new { app = resources.App, id = Id };
AddDeleteLink("delete", resources.Url<BackupsController>(x => nameof(x.DeleteBackup), values));
AddDeleteLink("delete",
resources.Url<BackupsController>(x => nameof(x.DeleteBackup), values));
}
if (resources.CanDownloadBackup)
{
var values = new { app = resources.App, appId = resources.AppId, id = Id };
AddGetLink("download", resources.Url<BackupContentController>(x => nameof(x.GetBackupContentV2), values));
AddGetLink("download",
resources.Url<BackupContentController>(x => nameof(x.GetBackupContentV2), values));
}
return this;

3
backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs

@ -37,7 +37,8 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models
if (resources.CanCreateBackup)
{
AddPostLink("create", resources.Url<BackupsController>(x => nameof(x.PostBackup), values));
AddPostLink("create",
resources.Url<BackupsController>(x => nameof(x.PostBackup), values));
}
return this;

18
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -169,21 +169,24 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
{
var versioned = new { app, schema, values.id, version = Version - 1 };
AddGetLink("previous", resources.Url<ContentsController>(x => nameof(x.GetContentVersion), versioned));
AddGetLink("previous",
resources.Url<ContentsController>(x => nameof(x.GetContentVersion), versioned));
}
if (NewStatus != null)
{
if (resources.CanDeleteContentVersion(schema))
{
AddDeleteLink("draft/delete", resources.Url<ContentsController>(x => nameof(x.DeleteVersion), values));
AddDeleteLink("draft/delete",
resources.Url<ContentsController>(x => nameof(x.DeleteVersion), values));
}
}
else if (Status == Status.Published)
{
if (resources.CanCreateContentVersion(schema))
{
AddPostLink("draft/create", resources.Url<ContentsController>(x => nameof(x.CreateDraft), values));
AddPostLink("draft/create",
resources.Url<ContentsController>(x => nameof(x.CreateDraft), values));
}
}
@ -202,17 +205,20 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
if (!content.IsSingleton && resources.CanDeleteContent(schema))
{
AddDeleteLink("delete", resources.Url<ContentsController>(x => nameof(x.DeleteContent), values));
AddDeleteLink("delete",
resources.Url<ContentsController>(x => nameof(x.DeleteContent), values));
}
if (content.CanUpdate && resources.CanUpdateContent(schema))
{
AddPatchLink("patch", resources.Url<ContentsController>(x => nameof(x.PatchContent), values));
AddPatchLink("patch",
resources.Url<ContentsController>(x => nameof(x.PatchContent), values));
}
if (content.CanUpdate && resources.CanUpdateContent(schema))
{
AddPutLink("update", resources.Url<ContentsController>(x => nameof(x.PutContent), values));
AddPutLink("update",
resources.Url<ContentsController>(x => nameof(x.PutContent), values));
}
return this;

6
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs

@ -66,13 +66,15 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
if (resources.CanCreateContent(values.schema))
{
AddPostLink("create", resources.Url<ContentsController>(x => nameof(x.PostContent), values));
AddPostLink("create",
resources.Url<ContentsController>(x => nameof(x.PostContent), values));
if (resources.CanChangeStatus(values.schema) && await workflow.CanPublishInitialAsync(schema, resources.Context.User))
{
var publishValues = new { values.app, values.schema, publish = true };
AddPostLink("create/publish", resources.Url<ContentsController>(x => nameof(x.PostContent), publishValues));
AddPostLink("create/publish",
resources.Url<ContentsController>(x => nameof(x.PostContent), publishValues));
}
}
}

9
backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs

@ -40,16 +40,19 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
if (!IsResetting)
{
AddPutLink("reset", resources.Url<EventConsumersController>(x => nameof(x.ResetEventConsumer), values));
AddPutLink("reset",
resources.Url<EventConsumersController>(x => nameof(x.ResetEventConsumer), values));
}
if (IsStopped)
{
AddPutLink("start", resources.Url<EventConsumersController>(x => nameof(x.StartEventConsumer), values));
AddPutLink("start",
resources.Url<EventConsumersController>(x => nameof(x.StartEventConsumer), values));
}
else
{
AddPutLink("stop", resources.Url<EventConsumersController>(x => nameof(x.StopEventConsumer), values));
AddPutLink("stop",
resources.Url<EventConsumersController>(x => nameof(x.StopEventConsumer), values));
}
}

24
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs

@ -113,44 +113,52 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
{
if (IsEnabled)
{
AddPutLink("disable", resources.Url<RulesController>(x => nameof(x.DisableRule), values));
AddPutLink("disable",
resources.Url<RulesController>(x => nameof(x.DisableRule), values));
}
else
{
AddPutLink("enable", resources.Url<RulesController>(x => nameof(x.EnableRule), values));
AddPutLink("enable",
resources.Url<RulesController>(x => nameof(x.EnableRule), values));
}
}
if (resources.CanUpdateRule)
{
AddPutLink("update", resources.Url<RulesController>(x => nameof(x.PutRule), values));
AddPutLink("update",
resources.Url<RulesController>(x => nameof(x.PutRule), values));
}
if (resources.CanRunRuleEvents)
{
AddPutLink("trigger", resources.Url<RulesController>(x => nameof(x.TriggerRule), values));
AddPutLink("trigger",
resources.Url<RulesController>(x => nameof(x.TriggerRule), values));
if (canRun && ruleRunnerService.CanRunRule(rule))
{
AddPutLink("run", resources.Url<RulesController>(x => nameof(x.PutRuleRun), values));
AddPutLink("run",
resources.Url<RulesController>(x => nameof(x.PutRuleRun), values));
}
if (canRun && ruleRunnerService.CanRunFromSnapshots(rule))
{
var snaphshotValues = new { values.app, values.id, fromSnapshots = true };
AddPutLink("run/snapshots", resources.Url<RulesController>(x => nameof(x.PutRuleRun), snaphshotValues));
AddPutLink("run/snapshots",
resources.Url<RulesController>(x => nameof(x.PutRuleRun), snaphshotValues));
}
}
if (resources.CanReadRuleEvents)
{
AddGetLink("logs", resources.Url<RulesController>(x => nameof(x.GetEvents), values));
AddGetLink("logs",
resources.Url<RulesController>(x => nameof(x.GetEvents), values));
}
if (resources.CanDeleteRule)
{
AddDeleteLink("delete", resources.Url<RulesController>(x => nameof(x.DeleteRule), values));
AddDeleteLink("delete",
resources.Url<RulesController>(x => nameof(x.DeleteRule), values));
}
return this;

6
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs

@ -80,12 +80,14 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
if (resources.CanUpdateRuleEvents)
{
AddPutLink("update", resources.Url<RulesController>(x => nameof(x.PutEvent), values));
AddPutLink("update",
resources.Url<RulesController>(x => nameof(x.PutEvent), values));
}
if (resources.CanDeleteRuleEvents && NextAttempt != null)
{
AddDeleteLink("cancel", resources.Url<RulesController>(x => nameof(x.DeleteEvent), values));
AddDeleteLink("cancel",
resources.Url<RulesController>(x => nameof(x.DeleteEvent), values));
}
return this;

6
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs

@ -48,11 +48,13 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
{
var routeValues = new { values.app, id = ruleId };
AddDeleteLink("cancel", resources.Url<RulesController>(x => nameof(x.DeleteRuleEvents), routeValues));
AddDeleteLink("cancel",
resources.Url<RulesController>(x => nameof(x.DeleteRuleEvents), routeValues));
}
else
{
AddDeleteLink("cancel", resources.Url<RulesController>(x => nameof(x.DeleteEvents), values));
AddDeleteLink("cancel",
resources.Url<RulesController>(x => nameof(x.DeleteEvents), values));
}
}

9
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs

@ -49,17 +49,20 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
if (resources.CanCreateRule)
{
AddPostLink("create", resources.Url<RulesController>(x => nameof(x.PostRule), values));
AddPostLink("create",
resources.Url<RulesController>(x => nameof(x.PostRule), values));
}
if (resources.CanReadRuleEvents)
{
AddGetLink("events", resources.Url<RulesController>(x => nameof(x.GetEvents), values));
AddGetLink("events",
resources.Url<RulesController>(x => nameof(x.GetEvents), values));
}
if (resources.CanDeleteRuleEvents && runningRuleId != null)
{
AddDeleteLink("run/cancel", resources.Url<RulesController>(x => nameof(x.DeleteRuleRun), values));
AddDeleteLink("run/cancel",
resources.Url<RulesController>(x => nameof(x.DeleteRuleRun), values));
}
return this;

27
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs

@ -108,44 +108,53 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
var values = new { app = resources.App, schema, id = FieldId };
AddPutLink("update", resources.Url<SchemaFieldsController>(x => nameof(x.PutField), values));
AddPutLink("update",
resources.Url<SchemaFieldsController>(x => nameof(x.PutField), values));
if (IsHidden)
{
AddPutLink("show", resources.Url<SchemaFieldsController>(x => nameof(x.ShowField), values));
AddPutLink("show",
resources.Url<SchemaFieldsController>(x => nameof(x.ShowField), values));
}
else
{
AddPutLink("hide", resources.Url<SchemaFieldsController>(x => nameof(x.HideField), values));
AddPutLink("hide",
resources.Url<SchemaFieldsController>(x => nameof(x.HideField), values));
}
if (IsDisabled)
{
AddPutLink("enable", resources.Url<SchemaFieldsController>(x => nameof(x.EnableField), values));
AddPutLink("enable",
resources.Url<SchemaFieldsController>(x => nameof(x.EnableField), values));
}
else
{
AddPutLink("disable", resources.Url<SchemaFieldsController>(x => nameof(x.DisableField), values));
AddPutLink("disable",
resources.Url<SchemaFieldsController>(x => nameof(x.DisableField), values));
}
if (Nested != null)
{
var parentValues = new { values.app, values.schema, parentId = FieldId };
AddPostLink("fields/add", resources.Url<SchemaFieldsController>(x => nameof(x.PostNestedField), parentValues));
AddPostLink("fields/add",
resources.Url<SchemaFieldsController>(x => nameof(x.PostNestedField), parentValues));
if (Nested.Count > 0)
{
AddPutLink("fields/order", resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedFieldOrdering), parentValues));
AddPutLink("fields/order",
resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedFieldOrdering), parentValues));
}
}
if (!IsLocked)
{
AddPutLink("lock", resources.Url<SchemaFieldsController>(x => nameof(x.LockField), values));
AddPutLink("lock",
resources.Url<SchemaFieldsController>(x => nameof(x.LockField), values));
}
AddDeleteLink("delete", resources.Url<SchemaFieldsController>(x => nameof(x.DeleteField), values));
AddDeleteLink("delete",
resources.Url<SchemaFieldsController>(x => nameof(x.DeleteField), values));
}
if (Nested != null)

21
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs

@ -53,32 +53,39 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
var values = new { app = resources.App, schema, parentId, id = FieldId };
AddPutLink("update", resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedField), values));
AddPutLink("update",
resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedField), values));
if (IsHidden)
{
AddPutLink("show", resources.Url<SchemaFieldsController>(x => nameof(x.ShowNestedField), values));
AddPutLink("show",
resources.Url<SchemaFieldsController>(x => nameof(x.ShowNestedField), values));
}
else
{
AddPutLink("hide", resources.Url<SchemaFieldsController>(x => nameof(x.HideNestedField), values));
AddPutLink("hide",
resources.Url<SchemaFieldsController>(x => nameof(x.HideNestedField), values));
}
if (IsDisabled)
{
AddPutLink("enable", resources.Url<SchemaFieldsController>(x => nameof(x.EnableNestedField), values));
AddPutLink("enable",
resources.Url<SchemaFieldsController>(x => nameof(x.EnableNestedField), values));
}
else
{
AddPutLink("disable", resources.Url<SchemaFieldsController>(x => nameof(x.DisableNestedField), values));
AddPutLink("disable",
resources.Url<SchemaFieldsController>(x => nameof(x.DisableNestedField), values));
}
if (!IsLocked)
{
AddPutLink("lock", resources.Url<SchemaFieldsController>(x => nameof(x.LockNestedField), values));
AddPutLink("lock",
resources.Url<SchemaFieldsController>(x => nameof(x.LockNestedField), values));
}
AddDeleteLink("delete", resources.Url<SchemaFieldsController>(x => nameof(x.DeleteNestedField), values));
AddDeleteLink("delete",
resources.Url<SchemaFieldsController>(x => nameof(x.DeleteNestedField), values));
}
}
}

64
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs

@ -115,13 +115,13 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// <summary>
/// The field rules.
/// </summary>
public List<FieldRuleDto> FieldRules { get; set; }
public List<FieldRuleDto> FieldRules { get; set; } = new List<FieldRuleDto>();
/// <summary>
/// The list of fields.
/// </summary>
[LocalizedRequired]
public List<FieldDto> Fields { get; set; }
public List<FieldDto> Fields { get; set; } = new List<FieldDto>();
public static SchemaDto FromDomain(ISchemaEntity schema, Resources resources)
{
@ -132,9 +132,10 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
SimpleMapper.Map(schema.SchemaDef.Scripts, result.Scripts);
SimpleMapper.Map(schema.SchemaDef.Properties, result.Properties);
result.FieldRules = schema.SchemaDef.FieldRules.Select(FieldRuleDto.FromDomain).ToList();
result.Fields = new List<FieldDto>();
foreach (var rule in schema.SchemaDef.FieldRules)
{
result.FieldRules.Add(FieldRuleDto.FromDomain(rule));
}
foreach (var field in schema.SchemaDef.Fields)
{
@ -154,51 +155,72 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
AddSelfLink(resources.Url<SchemasController>(x => nameof(x.GetSchema), values));
if (resources.CanReadContent(Name) && Type == SchemaType.Default)
if (resources.CanReadContent(Name) && Type != SchemaType.Component)
{
AddGetLink("contents", resources.Url<ContentsController>(x => nameof(x.GetContents), values));
AddGetLink("contents",
resources.Url<ContentsController>(x => nameof(x.GetContents), values));
}
if (resources.CanCreateContent(Name) && Type == SchemaType.Default)
{
AddPostLink("contents/create", resources.Url<ContentsController>(x => nameof(x.PostContent), values));
AddPostLink("contents/create/publish", resources.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true");
AddPostLink("contents/create",
resources.Url<ContentsController>(x => nameof(x.PostContent), values));
AddPostLink("contents/create/publish",
resources.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true");
}
if (resources.CanPublishSchema(Name))
{
if (IsPublished)
{
AddPutLink("unpublish", resources.Url<SchemasController>(x => nameof(x.UnpublishSchema), values));
AddPutLink("unpublish",
resources.Url<SchemasController>(x => nameof(x.UnpublishSchema), values));
}
else
{
AddPutLink("publish", resources.Url<SchemasController>(x => nameof(x.PublishSchema), values));
AddPutLink("publish",
resources.Url<SchemasController>(x => nameof(x.PublishSchema), values));
}
}
if (allowUpdate)
{
AddPostLink("fields/add", resources.Url<SchemaFieldsController>(x => nameof(x.PostField), values));
AddPostLink("fields/add",
resources.Url<SchemaFieldsController>(x => nameof(x.PostField), values));
AddPutLink("fields/order",
resources.Url<SchemaFieldsController>(x => nameof(x.PutSchemaFieldOrdering), values));
AddPutLink("fields/ui",
resources.Url<SchemaFieldsController>(x => nameof(x.PutSchemaUIFields), values));
AddPutLink("update",
resources.Url<SchemasController>(x => nameof(x.PutSchema), values));
AddPutLink("update/category",
resources.Url<SchemasController>(x => nameof(x.PutCategory), values));
AddPutLink("update/rules",
resources.Url<SchemasController>(x => nameof(x.PutRules), values));
AddPutLink("fields/ui", resources.Url<SchemaFieldsController>(x => nameof(x.PutSchemaUIFields), values));
AddPutLink("fields/order", resources.Url<SchemaFieldsController>(x => nameof(x.PutSchemaFieldOrdering), values));
AddPutLink("update/sync",
resources.Url<SchemasController>(x => nameof(x.PutSchemaSync), values));
AddPutLink("update", resources.Url<SchemasController>(x => nameof(x.PutSchema), values));
AddPutLink("update/category", resources.Url<SchemasController>(x => nameof(x.PutCategory), values));
AddPutLink("update/rules", resources.Url<SchemasController>(x => nameof(x.PutRules), values));
AddPutLink("update/sync", resources.Url<SchemasController>(x => nameof(x.PutSchemaSync), values));
AddPutLink("update/urls", resources.Url<SchemasController>(x => nameof(x.PutPreviewUrls), values));
AddPutLink("update/urls",
resources.Url<SchemasController>(x => nameof(x.PutPreviewUrls), values));
}
if (resources.CanUpdateSchemaScripts(Name))
{
AddPutLink("update/scripts", resources.Url<SchemasController>(x => nameof(x.PutScripts), values));
AddPutLink("update/scripts",
resources.Url<SchemasController>(x => nameof(x.PutScripts), values));
}
if (resources.CanDeleteSchema(Name))
{
AddDeleteLink("delete", resources.Url<SchemasController>(x => nameof(x.DeleteSchema), values));
AddDeleteLink("delete",
resources.Url<SchemasController>(x => nameof(x.DeleteSchema), values));
}
if (Fields != null)

3
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs

@ -35,7 +35,8 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
if (resources.CanCreateSchema)
{
AddPostLink("create", resources.Url<SchemasController>(x => nameof(x.PostSchema), values));
AddPostLink("create",
resources.Url<SchemasController>(x => nameof(x.PostSchema), values));
}
return this;

3
backend/src/Squidex/Areas/Api/Controllers/Search/Models/SearchResultDto.cs

@ -40,7 +40,8 @@ namespace Squidex.Areas.Api.Controllers.Search.Models
protected SearchResultDto CreateLinks(SearchResult searchResult)
{
AddGetLink("url", searchResult.Url);
AddGetLink("url",
searchResult.Url);
return this;
}

15
backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs

@ -19,24 +19,29 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
{
var result = new ResourcesDto();
result.AddGetLink("ping", resources.Url<PingController>(x => nameof(x.GetPing)));
result.AddGetLink("ping",
resources.Url<PingController>(x => nameof(x.GetPing)));
if (resources.CanReadEvents)
{
result.AddGetLink("admin/events", resources.Url<EventConsumersController>(x => nameof(x.GetEventConsumers)));
result.AddGetLink("admin/events",
resources.Url<EventConsumersController>(x => nameof(x.GetEventConsumers)));
}
if (resources.CanRestoreBackup)
{
result.AddGetLink("admin/restore", resources.Url<RestoreController>(x => nameof(x.GetRestoreJob)));
result.AddGetLink("admin/restore",
resources.Url<RestoreController>(x => nameof(x.GetRestoreJob)));
}
if (resources.CanReadUsers)
{
result.AddGetLink("admin/users", resources.Url<UserManagementController>(x => nameof(x.GetUsers)));
result.AddGetLink("admin/users",
resources.Url<UserManagementController>(x => nameof(x.GetUsers)));
}
result.AddGetLink("languages", resources.Url<LanguagesController>(x => nameof(x.GetLanguages)));
result.AddGetLink("languages",
resources.Url<LanguagesController>(x => nameof(x.GetLanguages)));
return result;
}

15
backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs

@ -72,23 +72,28 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
{
if (resources.CanLockUser && !IsLocked)
{
AddPutLink("lock", resources.Url<UserManagementController>(c => nameof(c.LockUser), values));
AddPutLink("lock",
resources.Url<UserManagementController>(c => nameof(c.LockUser), values));
}
if (resources.CanUnlockUser && IsLocked)
{
AddPutLink("unlock", resources.Url<UserManagementController>(c => nameof(c.UnlockUser), values));
AddPutLink("unlock",
resources.Url<UserManagementController>(c => nameof(c.UnlockUser), values));
}
AddDeleteLink("delete", resources.Url<UserManagementController>(x => nameof(x.DeleteUser), values));
AddDeleteLink("delete",
resources.Url<UserManagementController>(x => nameof(x.DeleteUser), values));
}
if (resources.CanUpdateUser)
{
AddPutLink("update", resources.Url<UserManagementController>(c => nameof(c.PutUser), values));
AddPutLink("update",
resources.Url<UserManagementController>(c => nameof(c.PutUser), values));
}
AddGetLink("picture", resources.Url<UsersController>(c => nameof(c.GetUserPicture), values));
AddGetLink("picture",
resources.Url<UsersController>(c => nameof(c.GetUserPicture), values));
return this;
}

3
backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs

@ -41,7 +41,8 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
if (context.CanCreateUser)
{
AddPostLink("create", context.Url<UserManagementController>(c => nameof(c.PostUser)));
AddPostLink("create",
context.Url<UserManagementController>(c => nameof(c.PostUser)));
}
return this;

10
backend/tools/TestSuite/TestSuite.ApiTests/AppClientsTests.cs

@ -95,10 +95,7 @@ namespace TestSuite.ApiTests
private async Task<ClientDto> CreateAsync()
{
var createRequest = new CreateClientDto
{
Id = id
};
var createRequest = new CreateClientDto { Id = id };
var clients = await _.Apps.PostClientAsync(appName, createRequest);
var client = clients.Items.Find(x => x.Id == id);
@ -108,10 +105,7 @@ namespace TestSuite.ApiTests
private async Task CreateAppAsync()
{
var createRequest = new CreateAppDto
{
Name = appName
};
var createRequest = new CreateAppDto { Name = appName };
await _.Apps.PostAppAsync(createRequest);
}

11
backend/tools/TestSuite/TestSuite.ApiTests/AppContributorsTests.cs

@ -69,10 +69,7 @@ namespace TestSuite.ApiTests
// STEP 1: Update contributor role.
var updateRequest = new AssignContributorDto
{
ContributorId = email, Role = "Owner"
};
var updateRequest = new AssignContributorDto { ContributorId = email, Role = "Owner" };
var contributors_2 = await _.Apps.PostContributorAsync(appName, updateRequest);
var contributor_2 = contributors_2.Items.Find(x => x.ContributorId == contributor.ContributorId);
@ -99,11 +96,7 @@ namespace TestSuite.ApiTests
private async Task<ContributorDto> InviteAsync()
{
var createInviteRequest = new AssignContributorDto
{
ContributorId = email,
Invite = true
};
var createInviteRequest = new AssignContributorDto { ContributorId = email, Invite = true };
var contributors = await _.Apps.PostContributorAsync(appName, createInviteRequest);
var contributor = contributors.Items.Find(x => x.ContributorName == email);

15
backend/tools/TestSuite/TestSuite.ApiTests/AppRolesTests.cs

@ -141,30 +141,21 @@ namespace TestSuite.ApiTests
private async Task AssignContributor(string role = null)
{
var assignRequest = new AssignContributorDto
{
ContributorId = contributor, Role = role, Invite = true
};
var assignRequest = new AssignContributorDto { ContributorId = contributor, Role = role, Invite = true };
await _.Apps.PostContributorAsync(_.AppName, assignRequest);
}
private async Task AssignClient(string role = null)
{
var updateRequest = new UpdateClientDto
{
Role = role
};
var updateRequest = new UpdateClientDto { Role = role };
await _.Apps.PutClientAsync(_.AppName, client, updateRequest);
}
private async Task<RoleDto> CreateRoleAsync(string name)
{
var createRequest = new AddRoleDto
{
Name = name
};
var createRequest = new AddRoleDto { Name = name };
var roles = await _.Apps.PostRoleAsync(_.AppName, createRequest);
var role = roles.Items.Find(x => x.Name == name);

14
backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs

@ -12,7 +12,6 @@ using Xunit;
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
#pragma warning disable SA1133 // Do not combine attributes
namespace TestSuite.ApiTests
{
@ -455,7 +454,7 @@ namespace TestSuite.ApiTests
Assert.Single(assets_1.Items, x => x.Id == asset_1.Id);
}
[Fact, Trait("Category", "NotAutomated")]
[Fact]
public async Task Should_delete_recursively()
{
// STEP 1: Create asset folder
@ -477,16 +476,7 @@ namespace TestSuite.ApiTests
// STEP 4: Delete folder.
await _.Assets.DeleteAssetFolderAsync(_.AppName, folder_1.Id);
// STEP 5: Wait for recursive deleter to delete the asset.
await Task.Delay(5000);
var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() =>
{
return _.Assets.GetAssetAsync(_.AppName, asset_1.Id);
});
Assert.Equal(404, ex.StatusCode);
Assert.True(await _.Assets.WaitForDeletionAsync(_.AppName, asset_1.Id, TimeSpan.FromSeconds(30)));
}
[Theory]

17
backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs

@ -8,6 +8,7 @@
using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures;
using TestSuite.Model;
using TestSuite.Utils;
using Xunit;
#pragma warning disable SA1300 // Element should begin with upper-case letter
@ -15,7 +16,6 @@ using Xunit;
namespace TestSuite.ApiTests
{
[Trait("Category", "NotAutomated")]
public class BackupTests : IClassFixture<ClientFixture>
{
private readonly string appName = Guid.NewGuid().ToString();
@ -31,6 +31,9 @@ namespace TestSuite.ApiTests
[Fact]
public async Task Should_backup_and_restore_app()
{
// Load the backup from another URL, because the public URL is might not be accessible for the server.
var backupUrl = TestHelpers.GetAndPrintValue("config:backupUrl", _.ServerUrl);
var appNameRestore = $"{appName}-restore";
// STEP 1: Create app
@ -44,16 +47,22 @@ namespace TestSuite.ApiTests
// STEP 3: Create backup
await _.Backups.PostBackupAsync(appNameRestore);
await _.Backups.PostBackupAsync(appName);
var backup = await _.Backups.WaitForBackupAsync(appName, TimeSpan.FromMinutes(2));
Assert.Equal(JobStatus.Completed, backup?.Status);
// STEP 3: Restore backup
var uri = new Uri(new Uri(_.ServerUrl, UriKind.Absolute), backup._links["download"].Href);
// STEP 4: Restore backup
var uri = new Uri(new Uri(backupUrl), backup._links["download"].Href);
var restoreRequest = new RestoreRequestDto { Url = uri, Name = appNameRestore };
await _.Backups.PostRestoreJobAsync(restoreRequest);
// STEP 5: Wait for the backup.
var restore = await _.Backups.WaitForRestoreAsync(uri, TimeSpan.FromMinutes(2));
Assert.Equal(JobStatus.Completed, restore?.Status);

1
backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs

@ -13,7 +13,6 @@ using Xunit;
namespace TestSuite.ApiTests
{
[Trait("Category", "NotAutomated")]
public class CDNTests : IClassFixture<CloudFixture>
{
public CloudFixture _ { get; }

2
backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs

@ -442,7 +442,7 @@ namespace TestSuite.ApiTests
Interlocked.Increment(ref numSuccess);
}
catch (SquidexException ex) when (ex.StatusCode is 409 or 412)
catch (SquidexException ex) when (ex.StatusCode is 409 or 412)
{
Interlocked.Increment(ref numErrors);
return;

2
backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs

@ -13,7 +13,7 @@ using Xunit;
namespace TestSuite.LoadTests
{
public class ReadingContentBenchmarks : IClassFixture<ReadingFixture>
{
{
public ReadingFixture _ { get; }
public ReadingContentBenchmarks(ReadingFixture fixture)

27
backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs

@ -11,6 +11,33 @@ namespace TestSuite
{
public static class ClientExtensions
{
public static async Task<bool> WaitForDeletionAsync(this IAssetsClient assetsClient, string app, string id, TimeSpan timeout)
{
try
{
using var cts = new CancellationTokenSource(timeout);
while (!cts.IsCancellationRequested)
{
try
{
await assetsClient.GetAssetAsync(app, id, cts.Token);
}
catch (SquidexManagementException ex) when (ex.StatusCode == 404)
{
return true;
}
await Task.Delay(200, cts.Token);
}
}
catch (OperationCanceledException)
{
}
return false;
}
public static async Task<BackupJobDto> WaitForBackupAsync(this IBackupsClient backupsClient, string app, TimeSpan timeout)
{
try

2
backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj

@ -16,7 +16,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.ClientLibrary" Version="8.24.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="8.25.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" />
</ItemGroup>

Loading…
Cancel
Save