From 49d61485e7e09750eba117fb2a95b4f18b5262da Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 13 Sep 2022 21:09:35 +0200 Subject: [PATCH] Teams (#920) * Started with teams. * Fixes and tests. * Update tests. * More fixes. * More test fixes. * Consistent command usage. * Started with frontend. * More progress. * More UI * Fix tests. * More tests and texts. * Fix tests. --- .gitignore | 1 + backend/i18n/frontend_en.json | 28 +- backend/i18n/frontend_it.json | 22 + backend/i18n/frontend_nl.json | 22 + backend/i18n/frontend_zh.json | 22 + backend/i18n/source/backend_en.json | 15 + backend/i18n/source/frontend_en.json | 28 +- .../{Apps/AppPlan.cs => AssignedPlan.cs} | 4 +- .../AppContributors.cs => Contributors.cs} | 18 +- .../FieldDescriptions.Designer.cs | 2 +- .../FieldDescriptions.resx | 2 +- .../Subscriptions/EventMessageWrapper.cs | 1 - .../Apps/MongoAppEntity.cs | 5 + .../Apps/MongoAppRepository.cs | 49 +- .../History/MongoHistoryEventRepository.cs | 27 +- .../Teams/MongoTeamEntity.cs | 38 ++ .../Teams/MongoTeamRepository.cs | 61 +++ .../AppProvider.cs | 45 +- .../Apps/AppHistoryEventsCreator.cs | 45 +- .../Apps/Commands/AddLanguage.cs | 2 +- .../Apps/Commands/AddRole.cs | 2 +- .../Apps/Commands/AddWorkflow.cs | 2 +- .../Apps/Commands/AssignContributor.cs | 2 +- .../Apps/Commands/AttachClient.cs | 2 +- .../Apps/Commands/ChangePlan.cs | 2 +- .../Apps/Commands/ConfigureAssetScripts.cs | 2 +- .../Apps/Commands/CreateApp.cs | 2 +- .../Apps/Commands/DeleteApp.cs | 2 +- .../Apps/Commands/DeleteRole.cs | 2 +- .../Apps/Commands/DeleteWorkflow.cs | 2 +- .../Apps/Commands/RemoveAppImage.cs | 2 +- .../Apps/Commands/RemoveContributor.cs | 2 +- .../Apps/Commands/RemoveLanguage.cs | 2 +- .../Apps/Commands/RevokeClient.cs | 2 +- .../{AppCommand.cs => TransferToTeam.cs} | 5 +- .../Apps/Commands/UpdateApp.cs | 2 +- .../Apps/Commands/UpdateAppSettings.cs | 2 +- .../Apps/Commands/UpdateClient.cs | 2 +- .../Apps/Commands/UpdateLanguage.cs | 2 +- .../Apps/Commands/UpdateRole.cs | 2 +- .../Apps/Commands/UpdateWorkflow.cs | 2 +- .../Apps/Commands/UploadAppImage.cs | 2 +- .../Apps/Commands/_AppCommand.cs | 30 ++ .../Apps/DomainObject/AppCommandMiddleware.cs | 2 +- .../DomainObject/AppDomainObject.State.cs | 30 +- .../Apps/DomainObject/AppDomainObject.cs | 87 ++-- .../Apps/DomainObject/Guards/GuardApp.cs | 36 +- .../Guards/GuardAppContributors.cs | 4 +- .../Apps/IAppEntity.cs | 10 +- .../Apps/Indexes/AppsIndex.cs | 20 +- .../Apps/Indexes/IAppsIndex.cs | 3 + .../Invitation/InvitationEventConsumer.cs | 96 ---- .../Invitation/InviteUserCommandMiddleware.cs | 65 --- .../Apps/Plans/ConfigAppLimitsPlan.cs | 43 -- .../Apps/Plans/ConfigAppPlansProvider.cs | 107 ----- .../Apps/Plans/IAppLimitsPlan.cs | 36 -- .../Apps/Plans/IAppPlansProvider.cs | 26 -- .../Plans/RestrictAppsCommandMiddleware.cs | 4 +- .../Apps/Plans/UsageGate.cs | 107 ----- .../Apps/Repositories/IAppRepository.cs | 3 + .../Assets/AssetUsageTracker.cs | 57 +-- .../Assets/AssetUsageTracker_EventHandling.cs | 30 +- .../{AssetCommand.cs => _AssetCommand.cs} | 16 +- ...olderCommand.cs => _AssetFolderCommand.cs} | 16 +- .../DomainObject/AssetDomainObject.State.cs | 5 - .../Assets/DomainObject/AssetDomainObject.cs | 2 +- .../AssetFolderDomainObject.State.cs | 4 - .../DomainObject/AssetFolderDomainObject.cs | 2 +- .../Assets/IAssetUsageTracker.cs | 12 +- .../Billing/ConfigPlansProvider.cs | 82 ++++ .../Billing/IAppUsageGate.cs | 40 ++ .../IBillingManager.cs} | 13 +- .../Billing/IBillingPlans.cs | 22 + .../{Apps/Plans => Billing}/Messages.cs | 2 +- .../NoopBillingManager.cs} | 22 +- .../Billing/Plan.cs | 38 ++ .../Plans => Billing}/PlanChangedResult.cs | 2 +- .../Billing/UsageGate.cs | 315 +++++++++++++ .../Plans => Billing}/UsageNotifierWorker.cs | 2 +- .../Comments/Commands/CommentTextCommand.cs | 2 +- .../Comments/Commands/DeleteComment.cs | 2 +- ...CommentsCommand.cs => _CommentsCommand.cs} | 35 +- .../DomainObject/CommentsCommandMiddleware.cs | 2 +- .../{ContentCommand.cs => _ContentCommand.cs} | 19 +- .../DomainObject/ContentDomainObject.cs | 2 +- .../GraphQL/Types/Assets/AssetActions.cs | 3 +- .../GraphQL/Types/Contents/ContentActions.cs | 2 +- .../Contents/Queries/ContentQueryService.cs | 2 +- .../Queries/Steps/EnrichWithWorkflows.cs | 4 +- .../Contents/Queries/Steps/ScriptContent.cs | 2 +- .../Contents/Text/TextIndexingProcess.cs | 2 +- .../Squidex.Domain.Apps.Entities/Context.cs | 14 +- .../History/HistoryEvent.cs | 2 +- .../History/HistoryService.cs | 65 ++- .../History/IHistoryService.cs | 2 +- .../History/NotifoService.cs | 82 ++-- .../IAppProvider.cs | 10 + .../SchemaCommand.cs => ITeamCommand.cs} | 8 +- .../Invitation/InvitationEventConsumer.cs | 131 ++++++ .../Invitation/InviteUserCommandMiddleware.cs | 90 ++++ .../{Apps => }/Invitation/InvitedResult.cs | 6 +- .../Notifications/INotificationSender.cs | 2 + .../Notifications/NoopNotificationSender.cs | 5 + .../Notifications/NotificationEmailSender.cs | 13 +- .../{RuleCommand.cs => _RuleCommand.cs} | 16 +- .../DomainObject/RuleDomainObject.State.cs | 4 - .../Rules/DomainObject/RuleDomainObject.cs | 2 +- .../Rules/RuleCommandMiddleware.cs | 2 +- .../Schemas/Commands/ChangeCategory.cs | 2 +- .../Schemas/Commands/ConfigureFieldRules.cs | 2 +- .../Schemas/Commands/ConfigurePreviewUrls.cs | 2 +- .../Schemas/Commands/ConfigureScripts.cs | 2 +- .../Schemas/Commands/ConfigureUIFields.cs | 2 +- .../Schemas/Commands/CreateSchema.cs | 9 +- .../Schemas/Commands/DeleteSchema.cs | 2 +- .../Schemas/Commands/ParentFieldCommand.cs | 2 +- .../Schemas/Commands/PublishSchema.cs | 2 +- .../Schemas/Commands/SynchronizeSchema.cs | 2 +- .../Schemas/Commands/UnpublishSchema.cs | 2 +- .../Schemas/Commands/UpdateSchema.cs | 2 +- ...hemaUpdateCommand.cs => _SchemaCommand.cs} | 13 +- .../DomainObject/SchemaDomainObject.State.cs | 18 - .../DomainObject/SchemaDomainObject.cs | 4 +- .../Schemas/Indexes/SchemasIndex.cs | 4 +- .../Teams/Commands/AssignContributor.cs | 24 + .../Teams/Commands/ChangePlan.cs | 16 + .../Commands/CreateTeam.cs} | 10 +- .../Teams/Commands/RemoveContributor.cs | 14 + .../Teams/Commands/UpdateTeam.cs | 14 + .../Teams/Commands/_TeamCommand.cs | 30 ++ .../Teams/DomainObject/Guards/GuardTeam.cs | 63 +++ .../Guards/GuardTeamContributors.cs | 80 ++++ .../DomainObject/TeamDomainObject.State.cs | 85 ++++ .../Teams/DomainObject/TeamDomainObject.cs | 225 +++++++++ .../Teams/ITeamEntity.cs | 24 + .../Teams/Indexes/ITeamsIndex.cs | 20 + .../Teams/Indexes/TeamsIndex.cs | 49 ++ .../Teams/Repositories/ITeamRepository.cs | 20 + .../Teams/TeamExtensions.cs | 19 + .../Teams/TeamHistoryEventsCreator.cs | 75 +++ .../Apps/AppPlanChanged.cs | 6 +- .../Apps/AppTransfered.cs | 18 + .../Teams/TeamContributorAssigned.cs | 23 + .../Teams/TeamContributorRemoved.cs | 17 + .../Teams/TeamCreated.cs | 17 + .../Teams/TeamEvent.cs | 16 + .../Teams/TeamPlanChanged.cs | 23 + .../Teams/TeamPlanReset.cs | 16 + .../Teams/TeamUpdated.cs | 17 + .../Squidex.Shared/PermissionExtensions.cs | 4 +- backend/src/Squidex.Shared/PermissionIds.cs | 77 ++- backend/src/Squidex.Shared/Texts.it.resx | 45 ++ backend/src/Squidex.Shared/Texts.nl.resx | 45 ++ backend/src/Squidex.Shared/Texts.resx | 45 ++ backend/src/Squidex.Shared/Texts.zh.resx | 45 ++ backend/src/Squidex.Web/ApiController.cs | 50 ++ .../src/Squidex.Web/ApiPermissionAttribute.cs | 9 +- .../EnrichWithTeamIdCommandMiddleware.cs | 55 +++ backend/src/Squidex.Web/ContextExtensions.cs | 16 +- backend/src/Squidex.Web/ITeamFeature.cs | 16 + .../Squidex.Web/Pipeline/ApiCostsFilter.cs | 10 +- .../src/Squidex.Web/Pipeline/AppResolver.cs | 27 +- .../src/Squidex.Web/Pipeline/ContextFilter.cs | 36 ++ .../src/Squidex.Web/Pipeline/TeamFeature.cs | 15 + .../src/Squidex.Web/Pipeline/TeamResolver.cs | 105 +++++ .../Squidex.Web/Pipeline/UsageMiddleware.cs | 21 +- backend/src/Squidex.Web/Resources.cs | 25 +- backend/src/Squidex.Web/UsageOptions.cs | 4 +- .../Controllers/Apps/AppClientsController.cs | 4 +- .../Apps/AppContributorsController.cs | 46 +- .../Apps/AppWorkflowsController.cs | 4 +- .../Api/Controllers/Apps/AppsController.cs | 66 ++- .../Api/Controllers/Apps/Models/AppDto.cs | 23 +- .../Apps/Models/CreateClientDto.cs | 2 +- .../Apps/Models/TransferToTeamDto.cs | 26 ++ .../Assets/AssetContentController.cs | 2 +- .../Assets/AssetFoldersController.cs | 6 +- .../Controllers/Assets/AssetsController.cs | 28 +- .../Api/Controllers/Assets/Models/AssetDto.cs | 4 +- .../Assets/Models/AssetFolderDto.cs | 4 +- .../Assets/Models/BulkUpdateAssetsJobDto.cs | 2 +- .../Assets/Models/CreateAssetFolderDto.cs | 2 +- .../{Apps/Models => }/AssignContributorDto.cs | 11 +- .../Backups/BackupContentController.cs | 6 +- .../Controllers/Backups/BackupsController.cs | 2 +- .../Backups/Models/BackupJobDto.cs | 2 +- .../Areas/Api/Controllers/BulkResultDto.cs | 4 +- .../Comments/CommentsController.cs | 28 +- .../Controllers/Comments/Models/CommentDto.cs | 2 +- .../UserNotificationsController.cs | 2 +- .../Contents/ContentsController.cs | 26 +- .../Models/BulkUpdateContentsJobDto.cs | 2 +- .../Controllers/Contents/Models/ContentDto.cs | 2 +- .../Contents/Models/ContentsDto.cs | 2 +- .../Contents/Models/ScheduleJobDto.cs | 2 +- .../{Apps/Models => }/ContributorDto.cs | 66 ++- .../{Apps/Models => }/ContributorsDto.cs | 57 ++- .../{Apps/Models => }/ContributorsMetadata.cs | 2 +- .../Controllers/History/HistoryController.cs | 25 +- .../Controllers/Plans/AppPlansController.cs | 33 +- .../Controllers/Plans/Models/ChangePlanDto.cs | 9 - .../Api/Controllers/Plans/Models/PlanDto.cs | 8 +- .../Models/{AppPlansDto.cs => PlansDto.cs} | 31 +- .../Controllers/Plans/TeamPlansController.cs | 100 ++++ .../Api/Controllers/Rules/Models/RuleDto.cs | 2 +- .../Controllers/Rules/Models/RuleEventDto.cs | 2 +- .../Api/Controllers/Rules/Models/RulesDto.cs | 2 +- .../ContentChangedRuleTriggerSchemaDto.cs | 2 +- .../Api/Controllers/Rules/RulesController.cs | 16 +- .../Controllers/Schemas/Models/FieldDto.cs | 2 +- .../Fields/ComponentFieldPropertiesDto.cs | 2 +- .../Fields/ComponentsFieldPropertiesDto.cs | 2 +- .../Fields/ReferencesFieldPropertiesDto.cs | 2 +- .../Schemas/Models/NestedFieldDto.cs | 2 +- .../Controllers/Schemas/Models/SchemaDto.cs | 2 +- .../Schemas/Models/UpsertSchemaDto.cs | 2 +- .../Schemas/SchemaFieldsController.cs | 28 +- .../Statistics/Models/CallsUsageDtoDto.cs | 4 +- .../Statistics/UsagesController.cs | 137 +++++- .../Controllers/Teams/Models/CreateTeamDto.cs | 27 ++ .../Api/Controllers/Teams/Models/TeamDto.cs | 99 ++++ .../Controllers/Teams/Models/UpdateTeamDto.cs | 27 ++ .../Teams/TeamContributorsController.cs | 148 ++++++ .../Api/Controllers/Teams/TeamsController.cs | 152 ++++++ .../Controllers/UI/Models/UISettingsDto.cs | 5 + .../Areas/Api/Controllers/UI/MyUIOptions.cs | 3 + .../Areas/Api/Controllers/UI/UIController.cs | 24 +- .../Api/Controllers/Users/Models/UserDto.cs | 2 +- .../Api/Controllers/Users/UsersController.cs | 4 +- .../Config/DynamicApplicationStore.cs | 1 - .../Controllers/Setup/SetupController.cs | 1 + .../Controllers/Setup/SetupVM.cs | 2 + .../IdentityServer/Views/Setup/Setup.cshtml | 9 + .../Middlewares/PortalRedirectMiddleware.cs | 10 +- .../Config/Authentication/OidcHandler.cs | 3 +- .../Squidex/Config/Domain/AssetServices.cs | 2 +- .../Squidex/Config/Domain/CommandsServices.cs | 20 +- .../Config/Domain/NotificationsServices.cs | 2 +- .../Squidex/Config/Domain/StoreServices.cs | 6 + .../Config/Domain/SubscriptionServices.cs | 13 +- .../src/Squidex/Config/Domain/TeamServices.cs | 21 + .../Config/Messaging/MessagingServices.cs | 2 +- backend/src/Squidex/Config/Web/WebServices.cs | 5 + backend/src/Squidex/Startup.cs | 1 + backend/src/Squidex/appsettings.json | 2 +- .../Model/Apps/AppContributorsJsonTests.cs | 2 +- .../Model/Apps/AppContributorsTests.cs | 2 +- .../Model/Apps/AppPlanTests.cs | 3 +- .../Model/Apps/RoleTests.cs | 12 +- .../Model/Apps/RolesTests.cs | 16 +- .../Model/Assets/AssetMetadataTests.cs | 40 +- .../Model/Contents/StatusTests.cs | 8 +- .../Model/Contents/TranslationStatusTests.cs | 16 +- .../Model/Contents/WorkflowJsonTests.cs | 4 +- .../Model/Contents/WorkflowTests.cs | 4 +- .../ConvertContent/ContentConversionTests.cs | 8 +- .../ConvertContent/FieldConvertersTests.cs | 72 +-- .../ConvertContent/ValueConvertersTests.cs | 36 +- .../ReferenceExtractionTests.cs | 40 +- .../GenerateFilters/FiltersTests.cs | 6 +- .../GenerateJsonSchema/JsonSchemaTests.cs | 6 +- .../RuleEventFormatterCompareTests.cs | 176 +++---- .../HandleRules/RuleEventFormatterTests.cs | 60 +-- .../HandleRules/RuleServiceTests.cs | 78 ++-- .../Scripting/ContentDataObjectTests.cs | 70 +-- .../Scripting/JintScriptEngineHelperTests.cs | 100 ++-- .../Scripting/JintScriptEngineTests.cs | 82 ++-- .../Scripting/ScriptingCompleterTests.cs | 72 +-- .../Subscriptions/EventMessageWrapperTests.cs | 4 +- .../Templates/FluidTemplateEngineTests.cs | 48 +- .../ValidateContent/AssetsFieldTests.cs | 4 +- .../ValidateContent/ComponentsFieldTests.cs | 6 +- .../ValidateContent/ReferencesFieldTests.cs | 4 +- .../Validators/AssetsValidatorTests.cs | 4 +- .../Validators/ReferencesValidatorTests.cs | 4 +- .../TestHelpers/TestUtils.cs | 14 +- .../AppProviderTests.cs | 80 +++- .../Apps/AppSettingsSearchSourceTests.cs | 142 +++--- .../Apps/AppUISettingsTests.cs | 3 +- .../Apps/BackupAppsTests.cs | 16 +- .../DomainObject/AppCommandMiddlewareTests.cs | 12 +- .../Apps/DomainObject/AppDomainObjectTests.cs | 233 +++++---- .../Apps/DomainObject/AppState.json | 6 +- .../Guards/GuardAppContributorsTests.cs | 54 +-- .../DomainObject/Guards/GuardAppRolesTests.cs | 3 +- .../Apps/DomainObject/Guards/GuardAppTests.cs | 115 ++++- .../Apps/Indexes/AppsIndexTests.cs | 15 + .../InvitationEventConsumerTests.cs | 193 -------- .../Plans/ConfigAppLimitsProviderTests.cs | 195 -------- .../Apps/Plans/UsageGateTests.cs | 167 ------- .../Apps/RolePermissionsProviderTests.cs | 12 +- .../Assets/AssetChangedTriggerHandlerTests.cs | 20 +- .../Assets/AssetUsageTrackerTests.cs | 82 +--- .../Assets/AssetsFluidExtensionTests.cs | 36 +- .../Assets/AssetsJintExtensionTests.cs | 76 +-- .../Assets/AssetsSearchSourceTests.cs | 12 +- .../Assets/BackupAssetsTests.cs | 12 +- .../Assets/DefaultAssetFileStoreTests.cs | 12 +- .../AssetCommandMiddlewareTests.cs | 44 +- .../DomainObject/AssetDomainObjectTests.cs | 52 +-- .../AssetFolderDomainObjectTests.cs | 20 +- .../AssetsBulkUpdateCommandMiddlewareTests.cs | 44 +- .../Assets/MongoDb/AssetMappingTests.cs | 4 +- .../Assets/MongoDb/AssetsQueryFixture.cs | 2 +- .../Assets/Queries/AssetEnricherTests.cs | 34 +- .../Assets/Queries/AssetLoaderTests.cs | 12 +- .../Assets/Queries/AssetQueryServiceTests.cs | 84 ++-- .../Backup/BackupServiceTests.cs | 12 +- .../Backup/StreamMapperTests.cs | 20 +- .../Billing/ConfigPlansProviderTests.cs | 134 ++++++ .../NoopBillingManagerTests.cs} | 35 +- .../Billing/UsageGateTests.cs | 441 ++++++++++++++++++ .../UsageNotifierWorkerTest.cs | 2 +- .../Comments/CommentTriggerHandlerTests.cs | 52 +-- .../Comments/CommentsLoaderTests.cs | 4 +- .../CommentsCommandMiddlewareTests.cs | 2 +- .../DomainObject/CommentsStreamTests.cs | 14 +- .../Contents/BackupContentsTests.cs | 6 +- .../ContentChangedTriggerHandlerTests.cs | 56 +-- .../Contents/ContentsSearchSourceTests.cs | 12 +- .../Counter/CounterJintExtensionTests.cs | 24 +- .../Contents/DefaultContentWorkflowTests.cs | 56 +-- .../ContentCommandMiddlewareTests.cs | 20 +- .../DomainObject/ContentDomainObjectTests.cs | 124 ++--- ...ontentsBulkUpdateCommandMiddlewareTests.cs | 144 +++--- .../Contents/DynamicContentWorkflowTests.cs | 108 ++--- .../GraphQL/GraphQLIntrospectionTests.cs | 4 +- .../Contents/GraphQL/GraphQLMutationTests.cs | 76 +-- .../Contents/GraphQL/GraphQLQueriesTests.cs | 100 ++-- .../GraphQL/GraphQLSubscriptionTests.cs | 17 +- .../Contents/GraphQL/GraphQLTestBase.cs | 20 +- .../Contents/GraphQL/TestContent.cs | 20 +- .../Contents/MongoDb/ContentMappingTests.cs | 4 +- .../Contents/MongoDb/ContentsQueryFixture.cs | 2 +- .../MongoDb/ContentsQueryIntegrationTests.cs | 14 +- .../Contents/MongoDb/StatusSerializerTests.cs | 4 +- .../Contents/Queries/ContentEnricherTests.cs | 10 +- .../Contents/Queries/ContentLoaderTests.cs | 12 +- .../Queries/ContentQueryServiceTests.cs | 54 +-- .../Queries/EnrichWithWorkflowsTests.cs | 10 +- .../Contents/Queries/ScriptContentTests.cs | 2 +- .../Contents/ReferencesFluidExtensionTests.cs | 8 +- .../Contents/ReferencesJintExtensionTests.cs | 20 +- .../Contents/Text/TextIndexerTestsBase.cs | 12 +- .../InvitationEventConsumerTests.cs | 334 +++++++++++++ .../InviteUserCommandMiddlewareTests.cs | 67 ++- .../NotificationEmailSenderTests.cs | 8 +- .../Rules/BackupRulesTests.cs | 2 +- .../RuleCommandMiddlewareTests.cs | 20 +- .../DomainObject/RuleDomainObjectTests.cs | 60 +-- .../Rules/ManualTriggerHandlerTests.cs | 9 +- .../Rules/Queries/RuleEnricherTests.cs | 8 +- .../Rules/Queries/RuleQueryServiceTests.cs | 4 +- .../Rules/RuleDequeuerWorkerTests.cs | 8 +- .../UsageTracking/UsageTriggerHandlerTests.cs | 12 +- .../Schemas/BackupSchemasTests.cs | 2 +- .../DomainObject/SchemaDomainObjectTests.cs | 132 +++--- .../SchemaChangedTriggerHandlerTests.cs | 21 +- .../Schemas/SchemasSearchSourceTests.cs | 24 +- .../Search/SearchManagerTests.cs | 30 +- .../Squidex.Domain.Apps.Entities.Tests.csproj | 3 + .../Guards/GuardTeamContributorsTests.cs | 198 ++++++++ .../DomainObject/Guards/GuardTeamTests.cs | 86 ++++ .../DomainObject/TeamDomainObjectTests.cs | 350 ++++++++++++++ .../Teams/Indexes/TeamsIndexTests.cs | 91 ++++ .../TestHelpers/HandlerTestBase.cs | 4 +- .../TestHelpers/Mocks.cs | 14 + .../DefaultUserResolverTests.cs | 32 +- .../DefaultUserServiceTests.cs | 36 +- .../Caching/QueryCacheTests.cs | 56 +-- .../Collections/ListDictionaryTests.cs | 36 +- .../Collections/ReadonlyDictionaryTests.cs | 12 +- .../Collections/ReadonlyListTests.cs | 12 +- .../Commands/CommandContextTests.cs | 4 +- .../Commands/DefaultDomainObjectCacheTests.cs | 8 +- .../Commands/DomainObjectTests.cs | 36 +- .../DomainIdTests.cs | 12 +- .../DomainObjectExceptionTests.cs | 28 +- .../Consume/EventConsumerManagerTests.cs | 4 +- .../WrongEventVersionExceptionTests.cs | 8 +- .../Json/Objects/JsonObjectTests.cs | 58 +-- .../LanguageTests.cs | 12 +- .../Log/BackgroundRequestLogStoreTests.cs | 12 +- .../MongoDb/BsonJsonSerializerTests.cs | 8 +- .../MongoDb/DomainIdSerializerTests.cs | 12 +- .../TypeConverterStringSerializerTests.cs | 4 +- .../Queries/PascalCasePathConverterTests.cs | 8 +- .../Queries/QueryOptimizationTests.cs | 32 +- .../States/InconsistentStateExceptionTests.cs | 12 +- .../States/SimpleStateTests.cs | 8 +- .../StringExtensionsTests.cs | 28 +- .../Tasks/SchedulerTests.cs | 20 +- .../Translations/TTests.cs | 20 +- .../UsageTracking/ApiUsageTrackerTests.cs | 8 +- .../BackgroundUsageTrackerTests.cs | 40 +- .../UsageTracking/CachingUsageTrackerTests.cs | 22 +- .../ValidationExceptionTests.cs | 6 +- .../ApiExceptionFilterAttributeTests.cs | 22 +- .../ETagCommandMiddlewareTests.cs | 14 +- .../EnrichWithAppIdCommandMiddlewareTests.cs | 4 +- ...nrichWithSchemaIdCommandMiddlewareTests.cs | 34 +- .../EnrichWithTeamIdCommandMiddlewareTests.cs | 72 +++ .../Pipeline/ApiCostsFilterTests.cs | 14 +- .../Pipeline/ApiPermissionUnifierTests.cs | 14 +- .../Pipeline/AppResolverTests.cs | 39 +- .../RequestExceptionMiddlewareTests.cs | 32 +- .../Pipeline/SchemaResolverTests.cs | 6 +- .../Pipeline/TeamResolverTests.cs | 238 ++++++++++ .../Pipeline/UsageMiddlewareTests.cs | 54 ++- ...pp_with_anonymous_read_access.verified.txt | 3 + ...p_with_anonymous_write_access.verified.txt | 3 + ...eationTests.Should_create_app.verified.txt | 3 + frontend/src/app/app.routes.ts | 22 +- .../state/event-consumers.state.ts | 14 +- .../administration/state/users.state.ts | 3 - .../src/app/features/apps/declarations.ts | 1 + frontend/src/app/features/apps/module.ts | 3 +- .../apps/pages/apps-page.component.html | 14 +- .../apps/pages/apps-page.component.scss | 13 +- .../apps/pages/apps-page.component.ts | 13 +- .../features/apps/pages/team.component.html | 29 ++ .../features/apps/pages/team.component.scss | 14 + .../app/features/apps/pages/team.component.ts | 25 + .../app/features/dashboard/declarations.ts | 13 +- frontend/src/app/features/dashboard/module.ts | 14 +- .../pages/cards/github-card.component.ts | 7 +- .../pages/dashboard-config.component.html | 8 +- .../pages/dashboard-config.component.ts | 59 +-- .../pages/dashboard-page.component.html | 36 +- .../pages/dashboard-page.component.ts | 33 ++ .../src/app/features/settings/declarations.ts | 1 + frontend/src/app/features/settings/module.ts | 3 +- .../pages/more/more-page.component.html | 29 +- .../pages/more/more-page.component.scss | 5 + .../pages/more/more-page.component.ts | 45 +- .../pages/plans/plans-page.component.html | 4 + .../settings/settings-area.component.html | 69 +-- .../settings/settings-menu.component.html | 68 +++ .../settings/settings-menu.component.scss | 0 .../settings-menu.component.ts} | 8 +- .../src/app/features/teams/declarations.ts | 20 + frontend/src/app/features/teams/internal.ts | 12 + .../features/teams/left-menu.component.html | 20 + .../features/teams/left-menu.component.scss | 0 .../app/features/teams/left-menu.component.ts | 20 + frontend/src/app/features/teams/module.ts | 100 ++++ .../contributor-add-form.component.html | 36 ++ .../contributor-add-form.component.scss | 14 + .../contributor-add-form.component.ts | 82 ++++ .../contributors/contributor.component.html | 18 + .../contributors/contributor.component.scss} | 0 .../contributors/contributor.component.ts | 37 ++ .../contributors-page.component.html | 61 +++ .../contributors-page.component.scss | 6 + .../contributors-page.component.ts | 48 ++ .../import-contributors-dialog.component.html | 63 +++ .../import-contributors-dialog.component.scss | 14 + .../import-contributors-dialog.component.ts | 95 ++++ .../dashboard/cards/apps-card.component.html | 13 + .../dashboard/cards/apps-card.component.scss} | 0 .../dashboard/cards/apps-card.component.ts | 40 ++ .../dashboard/dashboard-page.component.html | 67 +++ .../dashboard/dashboard-page.component.scss | 133 ++++++ .../dashboard/dashboard-page.component.ts | 156 +++++++ .../teams/pages/more/more-page.component.html | 31 ++ .../teams/pages/more/more-page.component.scss | 70 +++ .../teams/pages/more/more-page.component.ts | 63 +++ .../teams/pages/plans/plan.component.html | 56 +++ .../teams/pages/plans/plan.component.scss | 28 ++ .../teams/pages/plans/plan.component.ts | 34 ++ .../pages/plans/plans-page.component.html | 48 ++ .../pages/plans/plans-page.component.scss | 15 + .../teams/pages/plans/plans-page.component.ts | 43 ++ .../team-contributors.service.spec.ts | 143 ++++++ .../services/team-contributors.service.ts | 52 +++ .../teams/services/team-plans.service.spec.ts | 128 +++++ .../teams/services/team-plans.service.ts | 40 ++ .../teams/shared/settings-area.component.html | 9 + .../shared/settings-area.component.scss} | 0 .../teams/shared/settings-area.component.ts | 23 + .../teams/shared/settings-menu.component.html | 24 + .../teams/shared/settings-menu.component.scss | 0 .../teams/shared/settings-menu.component.ts | 20 + .../teams/state/team-contributors.forms.ts | 85 ++++ .../state/team-contributors.state.spec.ts | 206 ++++++++ .../teams/state/team-contributors.state.ts | 173 +++++++ .../teams/state/team-plans.state.spec.ts | 151 ++++++ .../features/teams/state/team-plans.state.ts | 147 ++++++ .../features/teams/team-area.component.html | 11 + .../features/teams/team-area.component.scss | 12 + .../app/features/teams/team-area.component.ts | 23 + frontend/src/app/framework/state.ts | 10 +- .../assets/asset-folder-dropdown.state.ts | 10 +- .../cards/api-calls-card.component.html | 4 +- .../cards/api-calls-card.component.scss} | 0 .../cards/api-calls-card.component.ts | 11 +- .../api-calls-summary-card.component.html | 0 .../api-calls-summary-card.component.scss} | 0 .../cards/api-calls-summary-card.component.ts | 7 +- .../cards/api-performance-card.component.html | 0 .../api-performance-card.component.scss} | 0 .../cards/api-performance-card.component.ts | 8 +- .../cards/api-traffic-card.component.html | 0 .../cards/api-traffic-card.component.scss} | 0 .../cards/api-traffic-card.component.ts | 8 +- .../api-traffic-summary-card.component.html | 0 .../api-traffic-summary-card.component.scss} | 0 .../api-traffic-summary-card.component.ts | 7 +- .../asset-uploads-count-card.component.html | 0 .../asset-uploads-count-card.component.scss} | 0 .../asset-uploads-count-card.component.ts | 8 +- .../asset-uploads-size-card.component.html | 0 .../asset-uploads-size-card.component.scss | 2 + .../asset-uploads-size-card.component.ts | 8 +- ...t-uploads-size-summary-card.component.html | 0 ...t-uploads-size-summary-card.component.scss | 2 + ...set-uploads-size-summary-card.component.ts | 7 +- .../cards/iframe-card.component.html | 0 .../cards/iframe-card.component.scss | 0 .../cards/iframe-card.component.ts | 6 +- .../cards/random-cat-card.component.html | 0 .../cards/random-cat-card.component.scss | 0 .../cards/random-cat-card.component.ts | 0 .../cards/random-dog-card.component.html | 0 .../cards/random-dog-card.component.scss | 0 .../cards/random-dog-card.component.ts | 0 .../components}/cards/shared.ts | 0 .../cards/support-card.component.html | 0 .../cards/support-card.component.scss | 2 + .../cards/support-card.component.ts | 17 + .../components/history/history.component.ts | 13 +- .../components/team-form.component.html | 39 ++ .../components/team-form.component.scss | 2 + .../shared/components/team-form.component.ts | 48 ++ frontend/src/app/shared/declarations.ts | 18 +- .../shared/guards/load-teams.guard.spec.ts | 32 ++ .../src/app/shared/guards/load-teams.guard.ts | 24 + .../guards/team-must-exist.guard.spec.ts | 50 ++ .../shared/guards/team-must-exist.guard.ts | 36 ++ .../shared/guards/unset-team.guard.spec.ts | 32 ++ .../src/app/shared/guards/unset-team.guard.ts | 24 + frontend/src/app/shared/internal.ts | 5 + frontend/src/app/shared/module.ts | 35 +- .../app/shared/services/apps.service.spec.ts | 57 ++- .../src/app/shared/services/apps.service.ts | 48 +- .../shared/services/contributors.service.ts | 71 +-- .../shared/services/history.service.spec.ts | 73 ++- .../app/shared/services/history.service.ts | 16 + .../app/shared/services/plans.service.spec.ts | 2 + .../src/app/shared/services/plans.service.ts | 67 +-- frontend/src/app/shared/services/shared.ts | 143 ++++++ .../app/shared/services/teams.service.spec.ts | 175 +++++++ .../src/app/shared/services/teams.service.ts | 123 +++++ .../shared/services/usages.service.spec.ts | 167 +++++-- .../src/app/shared/services/usages.service.ts | 46 +- .../src/app/shared/state/_test-helpers.ts | 13 +- frontend/src/app/shared/state/apps.forms.ts | 12 +- frontend/src/app/shared/state/apps.state.ts | 8 + .../app/shared/state/asset-scripts.state.ts | 10 +- .../app/shared/state/asset-uploader.state.ts | 9 +- .../src/app/shared/state/backups.state.ts | 14 +- .../src/app/shared/state/clients.state.ts | 14 +- .../src/app/shared/state/comments.state.ts | 14 +- .../src/app/shared/state/languages.state.ts | 28 +- frontend/src/app/shared/state/plans.state.ts | 17 +- frontend/src/app/shared/state/roles.state.ts | 14 +- frontend/src/app/shared/state/rules.state.ts | 14 +- .../src/app/shared/state/schemas.state.ts | 16 +- frontend/src/app/shared/state/teams.forms.ts | 34 ++ .../src/app/shared/state/teams.state.spec.ts | 144 ++++++ frontend/src/app/shared/state/teams.state.ts | 145 ++++++ .../src/app/shared/state/templates.state.ts | 14 +- frontend/src/app/shared/state/ui.state.ts | 2 +- .../src/app/shared/state/workflows.state.ts | 10 +- frontend/src/app/shell/declarations.ts | 1 + frontend/src/app/shell/module.ts | 3 +- .../shell/pages/app/left-menu.component.ts | 2 +- .../pages/internal/apps-menu.component.html | 90 ++-- .../pages/internal/apps-menu.component.ts | 8 +- .../pages/internal/teams-area.component.html | 1 + .../pages/internal/teams-area.component.scss | 12 + .../pages/internal/teams-area.component.ts | 16 + 582 files changed, 13267 insertions(+), 4671 deletions(-) rename backend/src/Squidex.Domain.Apps.Core.Model/{Apps/AppPlan.cs => AssignedPlan.cs} (86%) rename backend/src/Squidex.Domain.Apps.Core.Model/{Apps/AppContributors.cs => Contributors.cs} (66%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamEntity.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamRepository.cs rename backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/{AppCommand.cs => TransferToTeam.cs} (73%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/_AppCommand.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEventConsumer.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppLimitsPlan.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppLimitsPlan.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppPlansProvider.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs rename backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/{AssetCommand.cs => _AssetCommand.cs} (65%) rename backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/{AssetFolderCommand.cs => _AssetFolderCommand.cs} (63%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Billing/ConfigPlansProvider.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Billing/IAppUsageGate.cs rename backend/src/Squidex.Domain.Apps.Entities/{Apps/Plans/IAppPlanBillingManager.cs => Billing/IBillingManager.cs} (67%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingPlans.cs rename backend/src/Squidex.Domain.Apps.Entities/{Apps/Plans => Billing}/Messages.cs (93%) rename backend/src/Squidex.Domain.Apps.Entities/{Apps/Plans/NoopAppPlanBillingManager.cs => Billing/NoopBillingManager.cs} (65%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Billing/Plan.cs rename backend/src/Squidex.Domain.Apps.Entities/{Apps/Plans => Billing}/PlanChangedResult.cs (92%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs rename backend/src/Squidex.Domain.Apps.Entities/{Apps/Plans => Billing}/UsageNotifierWorker.cs (98%) rename backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/{CommentsCommand.cs => _CommentsCommand.cs} (51%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/{ContentCommand.cs => _ContentCommand.cs} (69%) rename backend/src/Squidex.Domain.Apps.Entities/{Schemas/Commands/SchemaCommand.cs => ITeamCommand.cs} (64%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Invitation/InviteUserCommandMiddleware.cs rename backend/src/Squidex.Domain.Apps.Entities/{Apps => }/Invitation/InvitedResult.cs (73%) rename backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/{RuleCommand.cs => _RuleCommand.cs} (64%) rename backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/{SchemaUpdateCommand.cs => _SchemaCommand.cs} (59%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/AssignContributor.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/ChangePlan.cs rename backend/src/Squidex.Domain.Apps.Entities/{Apps/Commands/AppUpdateCommand.cs => Teams/Commands/CreateTeam.cs} (63%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/RemoveContributor.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/UpdateTeam.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/_TeamCommand.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeam.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeamContributors.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.State.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/ITeamEntity.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/ITeamsIndex.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/TeamsIndex.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/Repositories/ITeamRepository.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/TeamExtensions.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Apps/AppTransfered.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorAssigned.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorRemoved.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Teams/TeamCreated.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Teams/TeamEvent.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanChanged.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanReset.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Teams/TeamUpdated.cs create mode 100644 backend/src/Squidex.Web/CommandMiddlewares/EnrichWithTeamIdCommandMiddleware.cs create mode 100644 backend/src/Squidex.Web/ITeamFeature.cs create mode 100644 backend/src/Squidex.Web/Pipeline/ContextFilter.cs create mode 100644 backend/src/Squidex.Web/Pipeline/TeamFeature.cs create mode 100644 backend/src/Squidex.Web/Pipeline/TeamResolver.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Apps/Models/TransferToTeamDto.cs rename backend/src/Squidex/Areas/Api/Controllers/{Apps/Models => }/AssignContributorDto.cs (78%) rename backend/src/Squidex/Areas/Api/Controllers/{Apps/Models => }/ContributorDto.cs (52%) rename backend/src/Squidex/Areas/Api/Controllers/{Apps/Models => }/ContributorsDto.cs (53%) rename backend/src/Squidex/Areas/Api/Controllers/{Apps/Models => }/ContributorsMetadata.cs (91%) rename backend/src/Squidex/Areas/Api/Controllers/Plans/Models/{AppPlansDto.cs => PlansDto.cs} (60%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Teams/Models/CreateTeamDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Teams/Models/TeamDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Teams/Models/UpdateTeamDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs create mode 100644 backend/src/Squidex/Config/Domain/TeamServices.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEventConsumerTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/ConfigAppLimitsProviderTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageGateTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/{Apps/Plans/NoopAppPlanBillingManagerTests.cs => Billing/NoopBillingManagerTests.cs} (51%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/{Apps/Plans => Billing}/UsageNotifierWorkerTest.cs (99%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/{Apps => }/Invitation/InviteUserCommandMiddlewareTests.cs (55%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamContributorsTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/TeamDomainObjectTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs create mode 100644 backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithTeamIdCommandMiddlewareTests.cs create mode 100644 backend/tests/Squidex.Web.Tests/Pipeline/TeamResolverTests.cs create mode 100644 frontend/src/app/features/apps/pages/team.component.html create mode 100644 frontend/src/app/features/apps/pages/team.component.scss create mode 100644 frontend/src/app/features/apps/pages/team.component.ts create mode 100644 frontend/src/app/features/settings/settings-menu.component.html create mode 100644 frontend/src/app/features/settings/settings-menu.component.scss rename frontend/src/app/features/{dashboard/pages/cards/support-card.component.ts => settings/settings-menu.component.ts} (65%) create mode 100644 frontend/src/app/features/teams/declarations.ts create mode 100644 frontend/src/app/features/teams/internal.ts create mode 100644 frontend/src/app/features/teams/left-menu.component.html create mode 100644 frontend/src/app/features/teams/left-menu.component.scss create mode 100644 frontend/src/app/features/teams/left-menu.component.ts create mode 100644 frontend/src/app/features/teams/module.ts create mode 100644 frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.html create mode 100644 frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.scss create mode 100644 frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.ts create mode 100644 frontend/src/app/features/teams/pages/contributors/contributor.component.html rename frontend/src/app/features/{dashboard/pages/cards/api-calls-card.component.scss => teams/pages/contributors/contributor.component.scss} (100%) create mode 100644 frontend/src/app/features/teams/pages/contributors/contributor.component.ts create mode 100644 frontend/src/app/features/teams/pages/contributors/contributors-page.component.html create mode 100644 frontend/src/app/features/teams/pages/contributors/contributors-page.component.scss create mode 100644 frontend/src/app/features/teams/pages/contributors/contributors-page.component.ts create mode 100644 frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.html create mode 100644 frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.scss create mode 100644 frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.ts create mode 100644 frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.html rename frontend/src/app/features/{dashboard/pages/cards/api-calls-summary-card.component.scss => teams/pages/dashboard/cards/apps-card.component.scss} (100%) create mode 100644 frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.ts create mode 100644 frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.html create mode 100644 frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.scss create mode 100644 frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.ts create mode 100644 frontend/src/app/features/teams/pages/more/more-page.component.html create mode 100644 frontend/src/app/features/teams/pages/more/more-page.component.scss create mode 100644 frontend/src/app/features/teams/pages/more/more-page.component.ts create mode 100644 frontend/src/app/features/teams/pages/plans/plan.component.html create mode 100644 frontend/src/app/features/teams/pages/plans/plan.component.scss create mode 100644 frontend/src/app/features/teams/pages/plans/plan.component.ts create mode 100644 frontend/src/app/features/teams/pages/plans/plans-page.component.html create mode 100644 frontend/src/app/features/teams/pages/plans/plans-page.component.scss create mode 100644 frontend/src/app/features/teams/pages/plans/plans-page.component.ts create mode 100644 frontend/src/app/features/teams/services/team-contributors.service.spec.ts create mode 100644 frontend/src/app/features/teams/services/team-contributors.service.ts create mode 100644 frontend/src/app/features/teams/services/team-plans.service.spec.ts create mode 100644 frontend/src/app/features/teams/services/team-plans.service.ts create mode 100644 frontend/src/app/features/teams/shared/settings-area.component.html rename frontend/src/app/features/{dashboard/pages/cards/api-performance-card.component.scss => teams/shared/settings-area.component.scss} (100%) create mode 100644 frontend/src/app/features/teams/shared/settings-area.component.ts create mode 100644 frontend/src/app/features/teams/shared/settings-menu.component.html create mode 100644 frontend/src/app/features/teams/shared/settings-menu.component.scss create mode 100644 frontend/src/app/features/teams/shared/settings-menu.component.ts create mode 100644 frontend/src/app/features/teams/state/team-contributors.forms.ts create mode 100644 frontend/src/app/features/teams/state/team-contributors.state.spec.ts create mode 100644 frontend/src/app/features/teams/state/team-contributors.state.ts create mode 100644 frontend/src/app/features/teams/state/team-plans.state.spec.ts create mode 100644 frontend/src/app/features/teams/state/team-plans.state.ts create mode 100644 frontend/src/app/features/teams/team-area.component.html create mode 100644 frontend/src/app/features/teams/team-area.component.scss create mode 100644 frontend/src/app/features/teams/team-area.component.ts rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/api-calls-card.component.html (76%) rename frontend/src/app/{features/dashboard/pages/cards/api-traffic-card.component.scss => shared/components/cards/api-calls-card.component.scss} (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/api-calls-card.component.ts (83%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/api-calls-summary-card.component.html (100%) rename frontend/src/app/{features/dashboard/pages/cards/api-traffic-summary-card.component.scss => shared/components/cards/api-calls-summary-card.component.scss} (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/api-calls-summary-card.component.ts (83%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/api-performance-card.component.html (100%) rename frontend/src/app/{features/dashboard/pages/cards/asset-uploads-count-card.component.scss => shared/components/cards/api-performance-card.component.scss} (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/api-performance-card.component.ts (88%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/api-traffic-card.component.html (100%) rename frontend/src/app/{features/dashboard/pages/cards/asset-uploads-size-card.component.scss => shared/components/cards/api-traffic-card.component.scss} (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/api-traffic-card.component.ts (89%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/api-traffic-summary-card.component.html (100%) rename frontend/src/app/{features/dashboard/pages/cards/asset-uploads-size-summary-card.component.scss => shared/components/cards/api-traffic-summary-card.component.scss} (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/api-traffic-summary-card.component.ts (83%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/asset-uploads-count-card.component.html (100%) rename frontend/src/app/{features/dashboard/pages/cards/support-card.component.scss => shared/components/cards/asset-uploads-count-card.component.scss} (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/asset-uploads-count-card.component.ts (85%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/asset-uploads-size-card.component.html (100%) create mode 100644 frontend/src/app/shared/components/cards/asset-uploads-size-card.component.scss rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/asset-uploads-size-card.component.ts (86%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/asset-uploads-size-summary-card.component.html (100%) create mode 100644 frontend/src/app/shared/components/cards/asset-uploads-size-summary-card.component.scss rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/asset-uploads-size-summary-card.component.ts (82%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/iframe-card.component.html (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/iframe-card.component.scss (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/iframe-card.component.ts (85%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/random-cat-card.component.html (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/random-cat-card.component.scss (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/random-cat-card.component.ts (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/random-dog-card.component.html (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/random-dog-card.component.scss (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/random-dog-card.component.ts (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/shared.ts (100%) rename frontend/src/app/{features/dashboard/pages => shared/components}/cards/support-card.component.html (100%) create mode 100644 frontend/src/app/shared/components/cards/support-card.component.scss create mode 100644 frontend/src/app/shared/components/cards/support-card.component.ts create mode 100644 frontend/src/app/shared/components/team-form.component.html create mode 100644 frontend/src/app/shared/components/team-form.component.scss create mode 100644 frontend/src/app/shared/components/team-form.component.ts create mode 100644 frontend/src/app/shared/guards/load-teams.guard.spec.ts create mode 100644 frontend/src/app/shared/guards/load-teams.guard.ts create mode 100644 frontend/src/app/shared/guards/team-must-exist.guard.spec.ts create mode 100644 frontend/src/app/shared/guards/team-must-exist.guard.ts create mode 100644 frontend/src/app/shared/guards/unset-team.guard.spec.ts create mode 100644 frontend/src/app/shared/guards/unset-team.guard.ts create mode 100644 frontend/src/app/shared/services/shared.ts create mode 100644 frontend/src/app/shared/services/teams.service.spec.ts create mode 100644 frontend/src/app/shared/services/teams.service.ts create mode 100644 frontend/src/app/shared/state/teams.forms.ts create mode 100644 frontend/src/app/shared/state/teams.state.spec.ts create mode 100644 frontend/src/app/shared/state/teams.state.ts create mode 100644 frontend/src/app/shell/pages/internal/teams-area.component.html create mode 100644 frontend/src/app/shell/pages/internal/teams-area.component.scss create mode 100644 frontend/src/app/shell/pages/internal/teams-area.component.ts diff --git a/.gitignore b/.gitignore index c0ea04e02..bbfd917df 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.suo *.user *.vs +*.received.txt .angular .awCache diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 1278def16..75043e30f 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -6,12 +6,14 @@ "api.pageTitle": "API", "api.title": "API", "apps.allApps": "All Apps", + "apps.allTeams": "All Teams", "apps.appLoadFailed": "Failed to load app. Please reload.", "apps.appNameHint": "You can only use letters, numbers and dashes and not more than 40 characters.", "apps.appNameValidationMessage": "Name can contain lower case letters (a-z), numbers and dashes between.", "apps.appNameWarning": "The app name cannot be changed later.", - "apps.appsButtonCreate": "Apps Overview", - "apps.appsButtonFallbackTitle": "Apps Overview", + "apps.appsButtonCreate": "Create App", + "apps.appsButtonCreateTeam": "Create Team", + "apps.appsButtonFallbackTitle": "Apps and Teams", "apps.archiveFailed": "Failed to archive app.", "apps.create": "Create App", "apps.createBlankApp": "New App", @@ -37,6 +39,10 @@ "apps.loadSettingsFailed": "Failed to update UI settings. Please reload.", "apps.removeImage": "Remove image", "apps.removeImageFailed": "Failed to remove app image. Please reload.", + "apps.transfer": "Transfer", + "apps.transferFailed": "Failed to transfer the app. Please reload.", + "apps.transferTitle": "Transfer to team", + "apps.transferWarning": "Teams are used to share subscriptions.", "apps.updateAssetScriptsFailed": "Failed to update asset scripts. Please reload.", "apps.updateFailed": "Failed to update app. Please reload.", "apps.updateSettingsFailed": "Failed to update UI settings. Please reload.", @@ -378,6 +384,7 @@ "common.tagAddSchema": ", to add schema", "common.tags": "Tags", "common.tagsAll": "All tags", + "common.teams": "Teams", "common.templates": "Templates", "common.time": "Time", "common.to": "To", @@ -531,6 +538,7 @@ "dashboard.apiDocumentationCard": "API Documentation", "dashboard.apiPerformanceCard": "API Performance (ms): {summary}ms avg", "dashboard.apiPerformanceChart": "API Performance Chart", + "dashboard.appsCard": "Apps", "dashboard.assetSizeCard": "Assets Size (MB", "dashboard.assetSizeLabel": "Total Size", "dashboard.assetSizeLimitLabel": "Total limit", @@ -564,7 +572,8 @@ "dashboard.trafficHeader": "Traffic (MB)", "dashboard.trafficLimitLabel": "Monthly limit", "dashboard.trafficSummaryCard": "API Traffic Summary", - "dashboard.welcomeText": "Welcome to **{app}** dashboard.", + "dashboard.welcomeText": "Welcome to App **{app}**.", + "dashboard.welcomeTextTeam": "Welcome to Team **{team}**.", "dashboard.welcomeTitle": "Hi {user}", "eventConsumers.count": "Count", "eventConsumers.loadFailed": "Failed to load event consumers. Please reload.", @@ -603,6 +612,7 @@ "news.title": "New Features", "notifications.empty": "No notifications yet", "notifo.subscripeTooltip": "Click this button to subscribe to all changes and to receive push notifications.", + "plans.allApps": "The subscription is shared between all apps of this team. Check the dashboard for the assigned apps.", "plans.billingPortal": "Billing Portal", "plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.", "plans.change": "Change", @@ -613,6 +623,7 @@ "plans.includedStorage": "Storage", "plans.includedTraffic": "Traffic", "plans.loadFailed": "Failed to load plans. Please reload.", + "plans.managedByTeam": "App has been assigned to a team and therefore the subscription is managed by the team.", "plans.noPlanConfigured": "No plan configured, this app has unlimited usage.", "plans.notPlanOwner": "You have not created the subscription, therefore you cannot change the plan.", "plans.perMonth": "Per Month", @@ -979,6 +990,17 @@ "start.loginHint": "The login button will open a new popup. Once you are logged in successful we will redirect you to the Squidex management portal.", "start.madeBy": "Proudly made by", "start.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2021", + "teams.create": "Create", + "teams.createFailed": "Failed to create team. Please reload.", + "teams.leave": "Leave team", + "teams.leaveConfirmText": "Do you really want to leave this team?", + "teams.leaveConfirmTitle": "Leave team.", + "teams.leaveFailed": "Failed to leavew team. Please reload.", + "teams.loadFailed": "Failed to load teams. Please reload.", + "teams.teamLoadFailed": "Failed to load team. Please reload.", + "teams.teamNameHint": "You can use all characters here.", + "teams.teamNameWarning": "The team name is only used as display name and can be changed later.", + "teams.updateFailed": "Failed to update team. Please reload.", "templates.cliHint": "Download the CLI at https://github.com/squidex/squidex-samples to use the templates.", "templates.loadFailed": "Failed to load templates. Please reload.", "templates.refreshTooltip": "Refresh Templates", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index d392c3fb0..3b2988e9a 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -6,11 +6,13 @@ "api.pageTitle": "API", "api.title": "API", "apps.allApps": "Tutte le Apps", + "apps.allTeams": "All Teams", "apps.appLoadFailed": "Non è stato possibile caricare l'App. Per favore ricarica.", "apps.appNameHint": "Puoi utilizzare solo lettere, numeri e trattini e non più di 40 caratteri.", "apps.appNameValidationMessage": "Il nome può contenere lettere minuscole (a-z), numeri e trattini all'interno.", "apps.appNameWarning": "Il nome della app non potrà essere cambiato in un secondo momento.", "apps.appsButtonCreate": "Nuova App", + "apps.appsButtonCreateTeam": "Create Team", "apps.appsButtonFallbackTitle": "Lista App", "apps.archiveFailed": "Failed to archive app.", "apps.create": "Crea un'App", @@ -37,6 +39,10 @@ "apps.loadSettingsFailed": "Failed to update UI settings. Please reload.", "apps.removeImage": "Rimuovi l'immagine", "apps.removeImageFailed": "Non è stato possibile rimuovere l'immagine dell'app. Per favore ricarica.", + "apps.transfer": "Transfer", + "apps.transferFailed": "Failed to transfer the app. Please reload.", + "apps.transferTitle": "Transfer to team", + "apps.transferWarning": "Teams are used to share subscriptions.", "apps.updateAssetScriptsFailed": "Failed to update asset scripts. Please reload.", "apps.updateFailed": "Non è stato possibile aggiornare l'app. Per favore ricarica.", "apps.updateSettingsFailed": "Failed to update UI settings. Please reload.", @@ -378,6 +384,7 @@ "common.tagAddSchema": ", aggiungi schema", "common.tags": "Tag", "common.tagsAll": "Tutti i tag", + "common.teams": "Teams", "common.templates": "Templates", "common.time": "Ora", "common.to": "To", @@ -531,6 +538,7 @@ "dashboard.apiDocumentationCard": "Documentazione delle API", "dashboard.apiPerformanceCard": "Performance(ms) delle API: {summary}ms avg", "dashboard.apiPerformanceChart": "Diagramma delle Performance delle API", + "dashboard.appsCard": "Apps", "dashboard.assetSizeCard": "Dimensione delle risorse (MB", "dashboard.assetSizeLabel": "Dimensione totale", "dashboard.assetSizeLimitLabel": "Limite totale", @@ -565,6 +573,7 @@ "dashboard.trafficLimitLabel": "Limite mensile", "dashboard.trafficSummaryCard": "Riepilogo del traffico delle API", "dashboard.welcomeText": "Benvenuto sulla dashboard **{app}**.", + "dashboard.welcomeTextTeam": "Welcome to Team **{team}**.", "dashboard.welcomeTitle": "Ciao {user}", "eventConsumers.count": "Conteggio", "eventConsumers.loadFailed": "Non è stato possibile caricare event consumers. Per favore ricarica.", @@ -603,6 +612,7 @@ "news.title": "Nuove funzionalità", "notifications.empty": "No notifications yet", "notifo.subscripeTooltip": "Fai clic su questo pulsante per iscriverti a tutte le modifiche e ricevere le notifiche push.", + "plans.allApps": "The subscription is shared between all apps of this team. Check the dashboard for the assigned apps.", "plans.billingPortal": "Portale di fatturazione", "plans.billingPortalHint": "Vai al portale di fatturazione per lo storico dei pagamenti e una panoramica per l'abbonamento.", "plans.change": "Cambia", @@ -613,6 +623,7 @@ "plans.includedStorage": "Spazio disco", "plans.includedTraffic": "Traffico", "plans.loadFailed": "Non è stato possibile caricare i piani. Per favore ricarica.", + "plans.managedByTeam": "App has been assigned to a team and therefore the subscription is managed by the team.", "plans.noPlanConfigured": "Nessun piano è stato impostato, quest'app ha un uso illimitato dello spazio su disco.", "plans.notPlanOwner": "Non hai creato nessun abbonamento, pertanto non è possibile cambiare il piano.", "plans.perMonth": "Al mese", @@ -979,6 +990,17 @@ "start.loginHint": "Il pulsante per accedere aprirà un popup. Una volta effettuato l'accesso sarai indirizzato al portale per la gestione di Squidex.", "start.madeBy": "Realizzato con orgoglio da", "start.madeByCopyright": "Sebastian Stehle e Collaboratori, 2016-2020", + "teams.create": "Create", + "teams.createFailed": "Failed to create team. Please reload.", + "teams.leave": "Leave team", + "teams.leaveConfirmText": "Do you really want to leave this team?", + "teams.leaveConfirmTitle": "Leave team.", + "teams.leaveFailed": "Failed to leavew team. Please reload.", + "teams.loadFailed": "Failed to load teams. Please reload.", + "teams.teamLoadFailed": "Failed to load team. Please reload.", + "teams.teamNameHint": "You can use all characters here.", + "teams.teamNameWarning": "The team name is only used as display name and can be changed later.", + "teams.updateFailed": "Failed to update team. Please reload.", "templates.cliHint": "Download the CLI at https://github.com/squidex/squidex-samples to use the templates.", "templates.loadFailed": "Failed to load templates. Please reload.", "templates.refreshTooltip": "Refresh Templates", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 90b01cdcf..6e69814db 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -6,11 +6,13 @@ "api.pageTitle": "API", "api.title": "API", "apps.allApps": "Alle apps", + "apps.allTeams": "All Teams", "apps.appLoadFailed": "Kan app niet laden. Laad opnieuw.", "apps.appNameHint": "Je kunt alleen letters, cijfers en streepjes gebruiken en niet meer dan 40 tekens.", "apps.appNameValidationMessage": "Naam mag kleine letters (a-z), cijfers en streepjes tussen bevatten.", "apps.appNameWarning": "De app-naam kan later niet worden gewijzigd.", "apps.appsButtonCreate": "Apps-overzicht", + "apps.appsButtonCreateTeam": "Create Team", "apps.appsButtonFallbackTitle": "Apps-overzicht", "apps.archiveFailed": "Failed to archive app.", "apps.create": "App maken", @@ -37,6 +39,10 @@ "apps.loadSettingsFailed": "Failed to update UI settings. Please reload.", "apps.removeImage": "Afbeelding verwijderen", "apps.removeImageFailed": "Verwijderen van app-afbeelding is mislukt. Laad opnieuw.", + "apps.transfer": "Transfer", + "apps.transferFailed": "Failed to transfer the app. Please reload.", + "apps.transferTitle": "Transfer to team", + "apps.transferWarning": "Teams are used to share subscriptions.", "apps.updateAssetScriptsFailed": "Failed to update asset scripts. Please reload.", "apps.updateFailed": "Update app mislukt. Laad opnieuw.", "apps.updateSettingsFailed": "Failed to update UI settings. Please reload.", @@ -378,6 +384,7 @@ "common.tagAddSchema": ", om schema toe te voegen", "common.tags": "Tags", "common.tagsAll": "Alle tags", + "common.teams": "Teams", "common.templates": "Templates", "common.time": "Tijd", "common.to": "Naar", @@ -531,6 +538,7 @@ "dashboard.apiDocumentationCard": "API-documentatie", "dashboard.apiPerformanceCard": "API-prestaties (ms): {summary} ms gem.", "dashboard.apiPerformanceChart": "API-prestatiegrafiek", + "dashboard.appsCard": "Apps", "dashboard.assetSizeCard": "Grootte van bestand (MB", "dashboard.assetSizeLabel": "Totale grootte", "dashboard.assetSizeLimitLabel": "Totale limiet", @@ -565,6 +573,7 @@ "dashboard.trafficLimitLabel": "Maandelijks limiet", "dashboard.trafficSummaryCard": "API Verkeer Samenvatting", "dashboard.welcomeText": "Welkom bij **{app}** dashboard.", + "dashboard.welcomeTextTeam": "Welcome to Team **{team}**.", "dashboard.welcomeTitle": "Hallo {user}", "eventConsumers.count": "Tellen", "eventConsumers.loadFailed": "Kan gebeurtenisgebruikers niet laden. Laad opnieuw.", @@ -603,6 +612,7 @@ "news.title": "Nieuwe functies", "notifications.empty": "Nog geen meldingen", "notifo.subscripeTooltip": "Klik op deze knop om je te abonneren op alle wijzigingen en om pushmeldingen te ontvangen.", + "plans.allApps": "The subscription is shared between all apps of this team. Check the dashboard for the assigned apps.", "plans.billingPortal": "Factureringsportal", "plans.billingPortalHint": "Ga naar het factureringsportaal voor betalingsgeschiedenis en abonnementsoverzicht.", "plans.change": "Wijzigen", @@ -613,6 +623,7 @@ "plans.includedStorage": "Opslag", "plans.includedTraffic": "Verkeer", "plans.loadFailed": "Laden van plannen is mislukt. Laad opnieuw.", + "plans.managedByTeam": "App has been assigned to a team and therefore the subscription is managed by the team.", "plans.noPlanConfigured": "Geen plan geconfigureerd, deze app heeft onbeperkt gebruik.", "plans.notPlanOwner": "Je hebt geen abonnement aangemaakt. Daarom kun je het plan niet wijzigen.", "plans.perMonth": "Per maand", @@ -979,6 +990,17 @@ "start.loginHint": "De login-knop opent een nieuwe pop-up. Zodra je succesvol bent ingelogd, zullen we je doorverwijzen naar het Squidex beheerportaal.", "start.madeBy": "Met trots gemaakt door", "start.madeByCopyright": "Sebastian Stehle en medewerkers, 2016-2020", + "teams.create": "Create", + "teams.createFailed": "Failed to create team. Please reload.", + "teams.leave": "Leave team", + "teams.leaveConfirmText": "Do you really want to leave this team?", + "teams.leaveConfirmTitle": "Leave team.", + "teams.leaveFailed": "Failed to leavew team. Please reload.", + "teams.loadFailed": "Failed to load teams. Please reload.", + "teams.teamLoadFailed": "Failed to load team. Please reload.", + "teams.teamNameHint": "You can use all characters here.", + "teams.teamNameWarning": "The team name is only used as display name and can be changed later.", + "teams.updateFailed": "Failed to update team. Please reload.", "templates.cliHint": "Download the CLI at https://github.com/squidex/squidex-samples to use the templates.", "templates.loadFailed": "Failed to load templates. Please reload.", "templates.refreshTooltip": "Refresh Templates", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 8f5b761ed..16be6a543 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -6,11 +6,13 @@ "api.pageTitle": "API", "api.title": "API", "apps.allApps": "所有应用程序", + "apps.allTeams": "All Teams", "apps.appLoadFailed": "加载应用失败。请重新加载。", "apps.appNameHint": "您只能使用字母、数字和破折号,并且不能超过 40 个字符。", "apps.appNameValidationMessage": "名称可以包含小写字母 (a-z)、数字和破折号。", "apps.appNameWarning": "以后不能更改应用名称。", "apps.appsButtonCreate": "应用概览", + "apps.appsButtonCreateTeam": "Create Team", "apps.appsButtonFallbackTitle": "应用概览", "apps.archiveFailed": "Failed to archive app.", "apps.create": "创建应用程序", @@ -37,6 +39,10 @@ "apps.loadSettingsFailed": "更新界面设置失败。请重新加载。", "apps.removeImage": "删除图片", "apps.removeImageFailed": "删除应用图片失败。请重新加载。", + "apps.transfer": "Transfer", + "apps.transferFailed": "Failed to transfer the app. Please reload.", + "apps.transferTitle": "Transfer to team", + "apps.transferWarning": "Teams are used to share subscriptions.", "apps.updateAssetScriptsFailed": "Failed to update asset scripts. Please reload.", "apps.updateFailed": "更新应用失败。请重新加载。", "apps.updateSettingsFailed": "更新界面设置失败。请重新加载。", @@ -378,6 +384,7 @@ "common.tagAddSchema": ", 添加Schemas", "common.tags": "标签", "common.tagsAll": "所有标签", + "common.teams": "Teams", "common.templates": "Templates", "common.time": "时间", "common.to": "To", @@ -531,6 +538,7 @@ "dashboard.apiDocumentationCard": "API 文档", "dashboard.apiPerformanceCard": "API 性能 (ms): {summary}ms avg", "dashboard.apiPerformanceChart": "API 性能图表", + "dashboard.appsCard": "Apps", "dashboard.assetSizeCard": "资源大小 (MB", "dashboard.assetSizeLabel": "总大小", "dashboard.assetSizeLimitLabel": "总限制", @@ -565,6 +573,7 @@ "dashboard.trafficLimitLabel": "每月限制", "dashboard.trafficSummaryCard": "API 流量汇总", "dashboard.welcomeText": "欢迎使用 **{app}** 仪表板。", + "dashboard.welcomeTextTeam": "Welcome to Team **{team}**.", "dashboard.welcomeTitle": "Hi {user}", "eventConsumers.count": "计数", "eventConsumers.loadFailed": "加载事件消费者失败。请重新加载。", @@ -603,6 +612,7 @@ "news.title": "新功能", "notifications.empty": "No notifications yet", "notifo.subscripeTooltip": "单击此按钮可订阅所有更改并接收推送通知。", + "plans.allApps": "The subscription is shared between all apps of this team. Check the dashboard for the assigned apps.", "plans.billingPortal": "计费门户", "plans.billingPortalHint": "前往账单门户查看付款历史和订阅概览。", "plans.change": "改变", @@ -613,6 +623,7 @@ "plans.includedStorage": "存储", "plans.includedTraffic": "交通", "plans.loadFailed": "加载计划失败。请重新加载。", + "plans.managedByTeam": "App has been assigned to a team and therefore the subscription is managed by the team.", "plans.noPlanConfigured": "未配置计划,此应用无限制使用。", "plans.notPlanOwner": "您尚未创建订阅。因此您无法更改计划。", "plans.perMonth": "每月", @@ -979,6 +990,17 @@ "start.loginHint": "登录按钮将打开一个新的弹出窗口。一旦您登录成功,我们会将您重定向到 Squidex 管理门户。", "start.madeBy": "自豪地制作", "start.madeByCopyright": "Sebastian Stehle 和贡献者,2016-2021", + "teams.create": "Create", + "teams.createFailed": "Failed to create team. Please reload.", + "teams.leave": "Leave team", + "teams.leaveConfirmText": "Do you really want to leave this team?", + "teams.leaveConfirmTitle": "Leave team.", + "teams.leaveFailed": "Failed to leavew team. Please reload.", + "teams.loadFailed": "Failed to load teams. Please reload.", + "teams.teamLoadFailed": "Failed to load team. Please reload.", + "teams.teamNameHint": "You can use all characters here.", + "teams.teamNameWarning": "The team name is only used as display name and can be changed later.", + "teams.updateFailed": "Failed to update team. Please reload.", "templates.cliHint": "Download the CLI at https://github.com/squidex/squidex-samples to use the templates.", "templates.loadFailed": "Failed to load templates. Please reload.", "templates.refreshTooltip": "Refresh Templates", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 859950cb3..74d071178 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -19,6 +19,7 @@ "apps.maximumTotalReached": "You cannot create more apps. Please contact the support to remove this restriction from your account.", "apps.nameAlreadyExists": "An app with the same name already exists.", "apps.notImage": "File is not an image", + "apps.plans.assignedToTeam": "Plan is managed by the team.", "apps.plans.notFound": "A plan with this id does not exist.", "apps.plans.notPlanOwner": "Plan can only changed from the user who configured the plan initially.", "apps.roles.defaultRoleNotRemovable": "Cannot delete a default role.", @@ -26,6 +27,8 @@ "apps.roles.nameAlreadyExists": "A role with the same name already exists.", "apps.roles.usedRoleByClientsNotRemovable": "Cannot remove a role when a client is assigned.", "apps.roles.usedRoleByContributorsNotRemovable": "Cannot remove a role when a contributor is assigned.", + "apps.transfer.planAssigned": "Subscription must be cancelled first before the app can be transfered.", + "apps.transfer.teamNotFound": "The team does not exist.", "assets.folderNotFound": "Asset folder does not exist.", "assets.folderRecursion": "Cannot add folder to its own child.", "assets.maxSizeReached": "You have reached your max asset size.", @@ -208,11 +211,14 @@ "exceptions.domainObjectDeleted": "Entity ({id}) has been deleted.", "exceptions.domainObjectNotFound": "Entity ({id}) does not exist.", "exceptions.domainObjectVersion": "Entity ({id}) requested version {expectedVersion}, but found {currentVersion}.", + "history.apps.assetScriptsConfigured": "updated asset scripts", "history.apps.clientAdded": "added client {[Id]} to app", "history.apps.clientRevoked": "revoked client {[Id]}", "history.apps.clientUpdated": "updated client {[Id]}", "history.apps.contributoreAssigned": "assigned {user:[Contributor]} as {[Role]}", "history.apps.contributoreRemoved": "removed {user:[Contributor]} from app", + "history.apps.imageRemoved": "removed app image", + "history.apps.imageUploaded": "uploaded a new app image", "history.apps.languagedAdded": "added language {[Language]}", "history.apps.languagedRemoved": "removed language {[Language]}", "history.apps.languagedSetToMaster": "changed master language to {[Language]}", @@ -223,6 +229,8 @@ "history.apps.roleDeleted": "deleted role {[Name]}", "history.apps.roleUpdated": "updated role {[Name]}", "history.apps.settingsUpdated": "updated UI settings", + "history.apps.transfered": "updated app to client", + "history.apps.updated": "updated general settings", "history.assets.replaced": "replaced asset.", "history.assets.updated": "updated asset.", "history.assets.uploaded": "uploaded asset.", @@ -248,6 +256,11 @@ "history.schemas.unpublished": "unpublished schema {[Name]}.", "history.schemas.updated": "updated schema {[Name]}.", "history.statusChanged": "changed status of {[Schema]} content to {[Status]}.", + "history.teams.contributoreAssigned": "assigned {user:[Contributor]} as {[Role]}", + "history.teams.contributoreRemoved": "removed {user:[Contributor]} from team", + "history.teams.planChanged": "changed plan to {[Plan]}", + "history.teams.planReset": "resetted plan", + "history.teams.updated": "updated general settings", "login.githubPrivateEmail": "Your email address is set to private in Github. Please set it to public to use Github login.", "rules.ruleAlreadyRunning": "Another rule is already running.", "schemas.dateTimeCalculatedDefaultAndDefaultError": "Calculated default value and default value cannot be used together.", @@ -293,6 +306,8 @@ "setup.ruleHttps.failure": " You are not accessing the site over https. If this warning is not correct then Squidex cannot detect https mode, because your instance is behind a reverse proxy such as nginx. Ensure that http headers are forwarded properly, via the X-Forwarded-* headers.", "setup.ruleHttps.success": "Congratulations, you are accessing your Squidex installation over a secure connection (https).", "setup.rules.headline": "System Checklist", + "setup.ruleTeamCreation.warningAdmins": "With your setup, only admins can create new teams. If you want to change this set UI__ONLYADMINSCANCREATETEAMS=false as environment variable.", + "setup.ruleTeamCreation.warningAll": "With your setup, every user can create new teams. If you want to change this set UI__ONLYADMINSCANCREATETEAMS=true as environment variable.", "setup.ruleUrl.failure": "You should access Squidex only over one canonical URL and configure this URL with the URLS__BASEURL environment variable. The current base URL {actual} does not match to the base url {configured}. This variable must point to the public URL under which your Squidex instance is available.", "setup.ruleUrl.success": "Congratulations, the URLS__BASEURL environment variable is configured properly. This variable must point to the public URL under which your Squidex instance is available.", "setup.title": "Installation", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 1278def16..75043e30f 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -6,12 +6,14 @@ "api.pageTitle": "API", "api.title": "API", "apps.allApps": "All Apps", + "apps.allTeams": "All Teams", "apps.appLoadFailed": "Failed to load app. Please reload.", "apps.appNameHint": "You can only use letters, numbers and dashes and not more than 40 characters.", "apps.appNameValidationMessage": "Name can contain lower case letters (a-z), numbers and dashes between.", "apps.appNameWarning": "The app name cannot be changed later.", - "apps.appsButtonCreate": "Apps Overview", - "apps.appsButtonFallbackTitle": "Apps Overview", + "apps.appsButtonCreate": "Create App", + "apps.appsButtonCreateTeam": "Create Team", + "apps.appsButtonFallbackTitle": "Apps and Teams", "apps.archiveFailed": "Failed to archive app.", "apps.create": "Create App", "apps.createBlankApp": "New App", @@ -37,6 +39,10 @@ "apps.loadSettingsFailed": "Failed to update UI settings. Please reload.", "apps.removeImage": "Remove image", "apps.removeImageFailed": "Failed to remove app image. Please reload.", + "apps.transfer": "Transfer", + "apps.transferFailed": "Failed to transfer the app. Please reload.", + "apps.transferTitle": "Transfer to team", + "apps.transferWarning": "Teams are used to share subscriptions.", "apps.updateAssetScriptsFailed": "Failed to update asset scripts. Please reload.", "apps.updateFailed": "Failed to update app. Please reload.", "apps.updateSettingsFailed": "Failed to update UI settings. Please reload.", @@ -378,6 +384,7 @@ "common.tagAddSchema": ", to add schema", "common.tags": "Tags", "common.tagsAll": "All tags", + "common.teams": "Teams", "common.templates": "Templates", "common.time": "Time", "common.to": "To", @@ -531,6 +538,7 @@ "dashboard.apiDocumentationCard": "API Documentation", "dashboard.apiPerformanceCard": "API Performance (ms): {summary}ms avg", "dashboard.apiPerformanceChart": "API Performance Chart", + "dashboard.appsCard": "Apps", "dashboard.assetSizeCard": "Assets Size (MB", "dashboard.assetSizeLabel": "Total Size", "dashboard.assetSizeLimitLabel": "Total limit", @@ -564,7 +572,8 @@ "dashboard.trafficHeader": "Traffic (MB)", "dashboard.trafficLimitLabel": "Monthly limit", "dashboard.trafficSummaryCard": "API Traffic Summary", - "dashboard.welcomeText": "Welcome to **{app}** dashboard.", + "dashboard.welcomeText": "Welcome to App **{app}**.", + "dashboard.welcomeTextTeam": "Welcome to Team **{team}**.", "dashboard.welcomeTitle": "Hi {user}", "eventConsumers.count": "Count", "eventConsumers.loadFailed": "Failed to load event consumers. Please reload.", @@ -603,6 +612,7 @@ "news.title": "New Features", "notifications.empty": "No notifications yet", "notifo.subscripeTooltip": "Click this button to subscribe to all changes and to receive push notifications.", + "plans.allApps": "The subscription is shared between all apps of this team. Check the dashboard for the assigned apps.", "plans.billingPortal": "Billing Portal", "plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.", "plans.change": "Change", @@ -613,6 +623,7 @@ "plans.includedStorage": "Storage", "plans.includedTraffic": "Traffic", "plans.loadFailed": "Failed to load plans. Please reload.", + "plans.managedByTeam": "App has been assigned to a team and therefore the subscription is managed by the team.", "plans.noPlanConfigured": "No plan configured, this app has unlimited usage.", "plans.notPlanOwner": "You have not created the subscription, therefore you cannot change the plan.", "plans.perMonth": "Per Month", @@ -979,6 +990,17 @@ "start.loginHint": "The login button will open a new popup. Once you are logged in successful we will redirect you to the Squidex management portal.", "start.madeBy": "Proudly made by", "start.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2021", + "teams.create": "Create", + "teams.createFailed": "Failed to create team. Please reload.", + "teams.leave": "Leave team", + "teams.leaveConfirmText": "Do you really want to leave this team?", + "teams.leaveConfirmTitle": "Leave team.", + "teams.leaveFailed": "Failed to leavew team. Please reload.", + "teams.loadFailed": "Failed to load teams. Please reload.", + "teams.teamLoadFailed": "Failed to load team. Please reload.", + "teams.teamNameHint": "You can use all characters here.", + "teams.teamNameWarning": "The team name is only used as display name and can be changed later.", + "teams.updateFailed": "Failed to update team. Please reload.", "templates.cliHint": "Download the CLI at https://github.com/squidex/squidex-samples to use the templates.", "templates.loadFailed": "Failed to load templates. Please reload.", "templates.refreshTooltip": "Refresh Templates", diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs b/backend/src/Squidex.Domain.Apps.Core.Model/AssignedPlan.cs similarity index 86% rename from backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/AssignedPlan.cs index efbddd2f8..36962a8d5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/AssignedPlan.cs @@ -9,9 +9,9 @@ using Squidex.Infrastructure; #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core { - public sealed record AppPlan(RefToken Owner, string PlanId) + public sealed record AssignedPlan(RefToken Owner, string PlanId) { public RefToken Owner { get; } = Guard.NotNull(Owner); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contributors.cs similarity index 66% rename from backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contributors.cs index bdc6bfe5f..d03a3f3fd 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contributors.cs @@ -9,23 +9,23 @@ using System.Diagnostics.Contracts; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core { - public sealed class AppContributors : ReadonlyDictionary + public sealed class Contributors : ReadonlyDictionary { - public static readonly AppContributors Empty = new AppContributors(); + public static readonly Contributors Empty = new Contributors(); - private AppContributors() + private Contributors() { } - public AppContributors(IDictionary inner) + public Contributors(IDictionary inner) : base(inner) { } [Pure] - public AppContributors Assign(string contributorId, string role) + public Contributors Assign(string contributorId, string role) { Guard.NotNullOrEmpty(contributorId); Guard.NotNullOrEmpty(role); @@ -35,11 +35,11 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return new AppContributors(updated); + return new Contributors(updated); } [Pure] - public AppContributors Remove(string contributorId) + public Contributors Remove(string contributorId) { Guard.NotNullOrEmpty(contributorId); @@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return new AppContributors(updated); + return new Contributors(updated); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs index 96461a015..98963a6d9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs @@ -214,7 +214,7 @@ namespace Squidex.Domain.Apps.Core { } /// - /// Looks up a localized string similar to The id of the parent folder. Empty for files without parent.. + /// Looks up a localized string similar to The ID of the parent folder. Empty for files without parent.. /// public static string AssetParentId { get { diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx index bda49992c..d1bd86513 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx @@ -169,7 +169,7 @@ The mime type. - The id of the parent folder. Empty for files without parent. + The ID of the parent folder. Empty for files without parent. The full path in the folder hierarchy as array of folder infos. diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs index 65264fd14..fe6ceca7c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; using Squidex.Messaging.Subscriptions; diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs index 2f7b79da1..87f62551f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs @@ -23,6 +23,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps [BsonElement("_ui")] public string[] IndexedUserIds { get; set; } + [BsonIgnoreIfDefault] + [BsonElement("_ti")] + public DomainId? IndexedTeamId { get; set; } + [BsonRequired] [BsonElement("_dl")] public bool IndexedDeleted { get; set; } @@ -44,6 +48,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps IndexedUserIds = users.ToArray(); IndexedCreated = Document.Created; IndexedDeleted = Document.IsDeleted; + IndexedTeamId = Document.TeamId; IndexedName = Document.Name; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index 5ed647b2e..07eea20f0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -10,7 +10,6 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Apps @@ -32,7 +31,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps .Ascending(x => x.IndexedName)), new CreateIndexModel( Index - .Ascending(x => x.IndexedUserIds)) + .Ascending(x => x.IndexedUserIds)), + new CreateIndexModel( + Index + .Ascending(x => x.IndexedTeamId)) }, ct); } @@ -42,53 +44,26 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return Collection.DeleteManyAsync(Filter.Eq(x => x.DocumentId, app.Id), ct); } - public async Task> QueryIdsAsync(string contributorId, - CancellationToken ct = default) - { - using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryIdsAsync")) - { - var find = Collection.Find(x => x.IndexedUserIds.Contains(contributorId) && !x.IndexedDeleted); - - return await QueryAsync(find, ct); - } - } - - public async Task> QueryIdsAsync(IEnumerable names, + public async Task> QueryAllAsync(string contributorId, IEnumerable names, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryAsync")) - { - var find = Collection.Find(x => names.Contains(x.IndexedName) && !x.IndexedDeleted); - - return await QueryAsync(find, ct); - } - } - - private static async Task> QueryAsync(IFindFluent find, - CancellationToken ct) - { - var entities = await find.SortBy(x => x.IndexedCreated).Only(x => x.DocumentId, x => x.IndexedName).ToListAsync(ct); - - var result = new Dictionary(); - - foreach (var entity in entities) + using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryAllAsync")) { - var indexedId = DomainId.Create(entity["_id"].AsString); - var indexedName = entity["_an"].AsString; + var entities = + await Collection.Find(x => (x.IndexedUserIds.Contains(contributorId) || names.Contains(x.IndexedName)) && !x.IndexedDeleted) + .ToListAsync(ct); - result[indexedName] = indexedId; + return entities.Select(x => (IAppEntity)x.Document).ToList(); } - - return result; } - public async Task> QueryAllAsync(string contributorId, IEnumerable names, + public async Task> QueryAllAsync(DomainId teamId, CancellationToken ct = default) { using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryAllAsync")) { var entities = - await Collection.Find(x => (x.IndexedUserIds.Contains(contributorId) || names.Contains(x.IndexedName)) && !x.IndexedDeleted) + await Collection.Find(x => x.IndexedTeamId == teamId) .ToListAsync(ct); return entities.Select(x => (IAppEntity)x.Document).ToList(); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs index 1c78b31fe..a1236dc9a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs @@ -23,6 +23,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History { cm.AutoMap(); + cm.MapProperty(x => x.OwnerId) + .SetElementName("AppId"); + cm.MapProperty(x => x.EventType) .SetElementName("Message"); }); @@ -45,13 +48,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History { new CreateIndexModel( Index - .Ascending(x => x.AppId) + .Ascending(x => x.OwnerId) .Ascending(x => x.Channel) .Descending(x => x.Created) .Descending(x => x.Version)), new CreateIndexModel( Index - .Ascending(x => x.AppId) + .Ascending(x => x.OwnerId) .Descending(x => x.Created) .Descending(x => x.Version)) }, ct); @@ -60,22 +63,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History async Task IDeleter.DeleteAppAsync(IAppEntity app, CancellationToken ct) { - await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); + await Collection.DeleteManyAsync(Filter.Eq(x => x.OwnerId, app.Id), ct); } - public async Task> QueryByChannelAsync(DomainId appId, string channelPrefix, int count, + public async Task> QueryByChannelAsync(DomainId ownerId, string channelPrefix, int count, CancellationToken ct = default) { - if (!string.IsNullOrWhiteSpace(channelPrefix)) - { - return await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix) - .SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count).ToListAsync(ct); - } - else - { - return await Collection.Find(x => x.AppId == appId) - .SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count).ToListAsync(ct); - } + var find = + !string.IsNullOrWhiteSpace(channelPrefix) ? + Collection.Find(x => x.OwnerId == ownerId && x.Channel == channelPrefix) : + Collection.Find(x => x.OwnerId == ownerId); + + return await find.SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count).ToListAsync(ct); } public Task InsertManyAsync(IEnumerable historyEvents, diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamEntity.cs new file mode 100644 index 000000000..157ad536a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamEntity.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson.Serialization.Attributes; +using NodaTime; +using Squidex.Domain.Apps.Entities.Teams.DomainObject; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Teams +{ + public sealed class MongoTeamEntity : MongoState + { + [BsonRequired] + [BsonElement("_ui")] + public string[] IndexedUserIds { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("_ct")] + public Instant IndexedCreated { get; set; } + + public override void Prepare() + { + var users = new HashSet + { + Document.CreatedBy.Identifier + }; + + users.AddRange(Document.Contributors.Keys); + + IndexedUserIds = users.ToArray(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamRepository.cs new file mode 100644 index 000000000..33d122942 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamRepository.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Domain.Apps.Entities.Teams.DomainObject; +using Squidex.Domain.Apps.Entities.Teams.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Teams +{ + public sealed class MongoTeamRepository : MongoSnapshotStoreBase, ITeamRepository + { + public MongoTeamRepository(IMongoDatabase database) + : base(database) + { + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index + .Ascending(x => x.IndexedUserIds)) + }, ct); + } + + public async Task> QueryAllAsync(string contributorId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoTeamRepository/QueryAllAsync")) + { + var entities = + await Collection.Find(x => x.IndexedUserIds.Contains(contributorId)) + .ToListAsync(ct); + + return entities.Select(x => (ITeamEntity)x.Document).ToList(); + } + } + + public async Task FindAsync(DomainId id, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoTeamRepository/FindAsync")) + { + var entity = + await Collection.Find(x => x.DocumentId == id) + .FirstOrDefaultAsync(ct); + + return entity?.Document; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs index c1fd1ebaa..a71ac826f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -12,6 +12,8 @@ using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Indexes; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Domain.Apps.Entities.Teams.Indexes; using Squidex.Infrastructure; using Squidex.Infrastructure.Security; @@ -23,14 +25,16 @@ namespace Squidex.Domain.Apps.Entities private readonly IAppsIndex indexForApps; private readonly IRulesIndex indexForRules; private readonly ISchemasIndex indexForSchemas; + private readonly ITeamsIndex indexForTeams; - public AppProvider(IAppsIndex indexForApps, IRulesIndex indexForRules, ISchemasIndex indexForSchemas, + public AppProvider(IAppsIndex indexForApps, IRulesIndex indexForRules, ISchemasIndex indexForSchemas, ITeamsIndex indexForTeams, ILocalCache localCache) { this.localCache = localCache; this.indexForApps = indexForApps; this.indexForRules = indexForRules; this.indexForSchemas = indexForSchemas; + this.indexForTeams = indexForTeams; } public async Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(DomainId appId, DomainId id, bool canCache = false, @@ -89,6 +93,19 @@ namespace Squidex.Domain.Apps.Entities return app; } + public async Task GetTeamAsync(DomainId teamId, + CancellationToken ct = default) + { + var cacheKey = TeamCacheKey(teamId); + + var team = await GetOrCreate(cacheKey, () => + { + return indexForTeams.GetTeamAsync(teamId, ct); + }); + + return team; + } + public async Task GetSchemaAsync(DomainId appId, string name, bool canCache = false, CancellationToken ct = default) { @@ -136,6 +153,27 @@ namespace Squidex.Domain.Apps.Entities return apps?.ToList() ?? new List(); } + public async Task> GetTeamAppsAsync(DomainId teamId, + CancellationToken ct = default) + { + var apps = await GetOrCreate($"GetTeamApps({teamId})", () => + { + return indexForApps.GetAppsForTeamAsync(teamId, ct)!; + }); + + return apps?.ToList() ?? new List(); + } + + public async Task> GetUserTeamsAsync(string userId, CancellationToken ct = default) + { + var teams = await GetOrCreate($"GetUserTeams({userId})", () => + { + return indexForTeams.GetTeamsAsync(userId, ct)!; + }); + + return teams?.ToList() ?? new List(); + } + public async Task> GetSchemasAsync(DomainId appId, CancellationToken ct = default) { @@ -207,6 +245,11 @@ namespace Squidex.Domain.Apps.Entities return $"APPS_NAME_{appName}"; } + private static string TeamCacheKey(DomainId teamId) + { + return $"TEAMS_ID{teamId}"; + } + private static string SchemaCacheKey(DomainId appId, DomainId id) { return $"SCHEMAS_ID_{appId}_{id}"; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs index c024bc7c8..8451b8494 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -13,7 +13,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Apps { - public class AppHistoryEventsCreator : HistoryEventsCreatorBase + public sealed class AppHistoryEventsCreator : HistoryEventsCreatorBase { public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry) : base(typeNameRegistry) @@ -62,6 +62,21 @@ namespace Squidex.Domain.Apps.Entities.Apps AddEventMessage( "history.apps.roleUpdated"); + + AddEventMessage( + "history.apps.assetScriptsConfigured"); + + AddEventMessage( + "history.apps.updated"); + + AddEventMessage( + "history.apps.transfered"); + + AddEventMessage( + "history.apps.imageUploaded"); + + AddEventMessage( + "history.apps.imageRemoved"); } private HistoryEvent? CreateEvent(IEvent @event) @@ -97,13 +112,28 @@ namespace Squidex.Domain.Apps.Entities.Apps case AppPlanReset e: return CreatePlansEvent(e); case AppSettingsUpdated e: - return CreateAppSettingsEvent(e); + return CreateAssetScriptsEvent(e); + case AppAssetsScriptsConfigured e: + return CreateGeneralEvent(e); + case AppUpdated e: + return CreateGeneralEvent(e); + case AppTransfered e: + return CreateGeneralEvent(e); + case AppImageUploaded e: + return CreateGeneralEvent(e); + case AppImageRemoved e: + return CreateGeneralEvent(e); } return null; } - private HistoryEvent CreateAppSettingsEvent(AppSettingsUpdated e) + private HistoryEvent CreateGeneralEvent(IEvent e) + { + return ForEvent(e, "general"); + } + + private HistoryEvent CreateAppSettingsEvent(IEvent e) { return ForEvent(e, "settings.appSettings"); } @@ -133,9 +163,14 @@ namespace Squidex.Domain.Apps.Entities.Apps return ForEvent(e, "settings.plan").Param("Plan", plan); } + private HistoryEvent CreateAssetScriptsEvent(IEvent e) + { + return ForEvent(e, "settings.assetScripts"); + } + protected override Task CreateEventCoreAsync(Envelope @event) { return Task.FromResult(CreateEvent(@event.Payload)); } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs index 7af7f6b43..0ce7500b3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class AddLanguage : AppUpdateCommand + public sealed class AddLanguage : AppCommand { public Language Language { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs index a536c7067..c1623759f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class AddRole : AppUpdateCommand + public sealed class AddRole : AppCommand { public string Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs index a96c76829..12c39549e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class AddWorkflow : AppUpdateCommand + public sealed class AddWorkflow : AppCommand { public DomainId WorkflowId { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs index aafa9b2f0..7e28f64d1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs @@ -9,7 +9,7 @@ using Roles = Squidex.Domain.Apps.Core.Apps.Role; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class AssignContributor : AppUpdateCommand + public sealed class AssignContributor : AppCommand { public string ContributorId { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs index debde745d..12cca6c65 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class AttachClient : AppUpdateCommand + public sealed class AttachClient : AppCommand { public string Id { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs index 28351c602..15be5174b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class ChangePlan : AppUpdateCommand + public sealed class ChangePlan : AppCommand { public bool FromCallback { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureAssetScripts.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureAssetScripts.cs index c322b0cca..13f5bf92d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureAssetScripts.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureAssetScripts.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Assets; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class ConfigureAssetScripts : AppUpdateCommand + public sealed class ConfigureAssetScripts : AppCommand { public AssetScripts? Scripts { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs index d9fa314cc..72526a486 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class CreateApp : AppCommand, IAggregateCommand + public sealed class CreateApp : AppCommandBase, IAggregateCommand { public DomainId AppId { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteApp.cs index 631e5d55e..8ea5a11c4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteApp.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class DeleteApp : AppUpdateCommand + public sealed class DeleteApp : AppCommand { } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs index 0ae4cf648..d18e03892 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class DeleteRole : AppUpdateCommand + public sealed class DeleteRole : AppCommand { public string Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs index 33a29e8c3..98b29fc7d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class DeleteWorkflow : AppUpdateCommand + public sealed class DeleteWorkflow : AppCommand { public DomainId WorkflowId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs index be603b50f..c88fa5c56 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class RemoveAppImage : AppUpdateCommand + public sealed class RemoveAppImage : AppCommand { } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs index 6fcf1ee30..d61035173 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class RemoveContributor : AppUpdateCommand + public sealed class RemoveContributor : AppCommand { public string ContributorId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs index dcc33a517..1fe4743f8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class RemoveLanguage : AppUpdateCommand + public sealed class RemoveLanguage : AppCommand { public Language Language { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs index 39d464afd..a545686a6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class RevokeClient : AppUpdateCommand + public sealed class RevokeClient : AppCommand { public string Id { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/TransferToTeam.cs similarity index 73% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/TransferToTeam.cs index 09d401854..a5e36102b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/TransferToTeam.cs @@ -6,12 +6,11 @@ // ========================================================================== using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public abstract class AppCommand : SquidexCommand, IAggregateCommand + public sealed class TransferToTeam : AppCommand { - public abstract DomainId AggregateId { get; } + public DomainId? TeamId { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs index 605decdd5..f47952812 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class UpdateApp : AppUpdateCommand + public sealed class UpdateApp : AppCommand { public string? Label { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateAppSettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateAppSettings.cs index 7093a4546..eae5da772 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateAppSettings.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateAppSettings.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Apps; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class UpdateAppSettings : AppUpdateCommand + public sealed class UpdateAppSettings : AppCommand { public AppSettings Settings { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs index 3a0909f85..11f15efb0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class UpdateClient : AppUpdateCommand + public sealed class UpdateClient : AppCommand { public string Id { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs index 18cae14c8..b643f678a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class UpdateLanguage : AppUpdateCommand + public sealed class UpdateLanguage : AppCommand { public Language Language { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs index d885d0eca..2465f4b5c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class UpdateRole : AppUpdateCommand + public sealed class UpdateRole : AppCommand { public string Name { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs index 16755127d..5978a00a9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class UpdateWorkflow : AppUpdateCommand + public sealed class UpdateWorkflow : AppCommand { public DomainId WorkflowId { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs index 65b1a535d..065fe76d2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs @@ -9,7 +9,7 @@ using Squidex.Assets; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class UploadAppImage : AppUpdateCommand + public sealed class UploadAppImage : AppCommand { public AssetFile File { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/_AppCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/_AppCommand.cs new file mode 100644 index 000000000..4f676f141 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/_AppCommand.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +#pragma warning disable MA0048 // File name must match type name + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public abstract class AppCommand : AppCommandBase, IAppCommand + { + public NamedId AppId { get; set; } + + public override DomainId AggregateId + { + get => AppId.Id; + } + } + + // This command is needed as marker for middlewares. + public abstract class AppCommandBase : SquidexCommand, IAggregateCommand + { + public abstract DomainId AggregateId { get; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs index 567a42484..7eda5795d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs @@ -13,7 +13,7 @@ using Squidex.Infrastructure.Validation; namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { - public sealed class AppCommandMiddleware : AggregateCommandMiddleware + public sealed class AppCommandMiddleware : AggregateCommandMiddleware { private readonly IAppImageStore appImageStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs index d179abd12..cf1b08bb2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Text.Json.Serialization; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Contents; @@ -29,17 +30,19 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject public string Description { get; set; } - public Roles Roles { get; set; } = Roles.Empty; + public DomainId? TeamId { get; set; } - public AppImage? Image { get; set; } + public Contributors Contributors { get; set; } = Contributors.Empty; - public AppPlan? Plan { get; set; } + public Roles Roles { get; set; } = Roles.Empty; + + public AssignedPlan? Plan { get; set; } public AppClients Clients { get; set; } = AppClients.Empty; - public AppSettings Settings { get; set; } = AppSettings.Empty; + public AppImage? Image { get; set; } - public AppContributors Contributors { get; set; } = AppContributors.Empty; + public AppSettings Settings { get; set; } = AppSettings.Empty; public AssetScripts AssetScripts { get; set; } = new AssetScripts(); @@ -64,26 +67,30 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject Id = e.AppId.Id; SimpleMapper.Map(e, this); - return true; } case AppUpdated e when Is.Change(Label, e.Label) || Is.Change(Description, e.Description): { SimpleMapper.Map(e, this); + return true; + } + case AppTransfered e when Is.Change(TeamId, e.TeamId): + { + SimpleMapper.Map(e, this); return true; } case AppSettingsUpdated e when Is.Change(Settings, e.Settings): return UpdateSettings(e.Settings); - case AppPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId): - return UpdatePlan(e.ToPlan()); - case AppAssetsScriptsConfigured e when Is.Change(e.Scripts, AssetScripts): return UpdateAssetScripts(e.Scripts); + case AppPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId): + return UpdatePlan(e.ToPlan()); + case AppPlanReset e when Plan != null: return UpdatePlan(null); @@ -150,7 +157,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject Plan = null; IsDeleted = true; - return true; } } @@ -158,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return false; } - private bool UpdateContributors(T @event, Func update) + private bool UpdateContributors(T @event, Func update) { var previous = Contributors; @@ -224,7 +230,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return true; } - private bool UpdatePlan(AppPlan? plan) + private bool UpdatePlan(AssignedPlan? plan) { Plan = plan; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs index 611d97f8e..918335352 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; @@ -42,12 +42,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject protected override bool CanAcceptCreation(ICommand command) { - return command is CreateApp; + return command is AppCommandBase; } protected override bool CanAccept(ICommand command) { - return command is AppUpdateCommand update && Equals(update?.AppId?.Id, Snapshot.Id); + return command is AppCommand update && Equals(update?.AppId?.Id, Snapshot.Id); } public override Task ExecuteAsync(IAggregateCommand command, @@ -75,6 +75,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return Snapshot; }, ct); + case TransferToTeam transfer: + return UpdateReturnAsync(transfer, async (c, ct) => + { + await GuardApp.CanTransfer(c, Snapshot, AppProvider(), ct); + + Transfer(c); + + return Snapshot; + }, ct); + case UpdateAppSettings updateSettings: return UpdateReturn(updateSettings, c => { @@ -258,7 +268,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject case DeleteApp delete: return UpdateAsync(delete, async (c, ct) => { - await Billing().UnsubscribeAsync(c.Actor.Identifier, Snapshot.NamedId(), default); + await BillingManager().UnsubscribeAsync(c.Actor.Identifier, Snapshot.NamedId(), default); DeleteApp(c); }, ct); @@ -279,7 +289,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject var result = await UpdateReturnAsync(changePlan, async (c, ct) => { - GuardApp.CanChangePlan(c, Snapshot, Plans()); + GuardApp.CanChangePlan(c, Snapshot, BillingPlans()); if (string.Equals(GetFreePlan()?.Id, c.PlanId, StringComparison.Ordinal)) { @@ -290,7 +300,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject if (!c.FromCallback) { - var redirectUri = await Billing().MustRedirectToPortalAsync(userId, Snapshot.NamedId(), c.PlanId, ct); + var redirectUri = await BillingManager().MustRedirectToPortalAsync(userId, Snapshot.NamedId(), c.PlanId, ct); if (redirectUri != null) { @@ -310,11 +320,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject if (result.Payload is PlanChangedResult { Unsubscribed: true, RedirectUri: null }) { - await Billing().UnsubscribeAsync(userId, Snapshot.NamedId(), default); + await BillingManager().UnsubscribeAsync(userId, Snapshot.NamedId(), default); } else if (result.Payload is PlanChangedResult { RedirectUri: null }) { - await Billing().SubscribeAsync(userId, Snapshot.NamedId(), changePlan.PlanId, default); + await BillingManager().SubscribeAsync(userId, Snapshot.NamedId(), changePlan.PlanId, default); } return result; @@ -324,23 +334,25 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { var appId = NamedId.Of(command.AppId, command.Name); - var events = new List + void RaiseInitial(T @event) where T : AppEvent { - CreateInitalEvent(command.Name) - }; + Raise(command, @event, appId); + } + + RaiseInitial(new AppCreated()); - if (command.Actor.IsUser) + var actor = command.Actor; + + if (actor.IsUser) { - events.Add(CreateInitialOwner(command.Actor)); + RaiseInitial(new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }); } - events.Add(CreateInitialSettings()); + var settings = serviceProvider.GetService()?.Settings; - foreach (var @event in events) + if (settings != null) { - @event.AppId = appId; - - Raise(command, @event); + RaiseInitial(new AppSettingsUpdated { Settings = settings }); } } @@ -359,6 +371,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject Raise(command, new AppUpdated()); } + private void Transfer(TransferToTeam command) + { + Raise(command, new AppTransfered()); + } + private void UpdateSettings(UpdateAppSettings command) { Raise(command, new AppSettingsUpdated()); @@ -454,38 +471,28 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject Raise(command, new AppDeleted()); } - private void Raise(T command, TEvent @event) where T : class where TEvent : AppEvent + private void Raise(T command, TEvent @event, NamedId? id = null) where T : class where TEvent : AppEvent { SimpleMapper.Map(command, @event); - @event.AppId ??= Snapshot.NamedId(); + @event.AppId ??= id ?? Snapshot.NamedId(); RaiseEvent(Envelope.Create(@event)); } - private static AppCreated CreateInitalEvent(string name) - { - return new AppCreated { Name = name }; - } - - private static AppContributorAssigned CreateInitialOwner(RefToken actor) - { - return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }; - } - - private AppSettingsUpdated CreateInitialSettings() + private IAppProvider AppProvider() { - return new AppSettingsUpdated { Settings = serviceProvider.GetRequiredService().Settings }; + return serviceProvider.GetRequiredService(); } - private IAppPlansProvider Plans() + private IBillingPlans BillingPlans() { - return serviceProvider.GetRequiredService(); + return serviceProvider.GetRequiredService(); } - private IAppPlanBillingManager Billing() + private IBillingManager BillingManager() { - return serviceProvider.GetRequiredService(); + return serviceProvider.GetRequiredService(); } private IUserResolver Users() @@ -493,14 +500,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return serviceProvider.GetRequiredService(); } - private IAppLimitsPlan GetFreePlan() + private Plan GetFreePlan() { - return Plans().GetFreePlan(); + return BillingPlans().GetFreePlan(); } - private IAppLimitsPlan GetPlan() + private Plan GetPlan() { - return Plans().GetPlanForApp(Snapshot).Plan; + return BillingPlans().GetActualPlan(Snapshot.Plan?.PlanId).Plan; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs index e7ed342e4..f11280caf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs @@ -6,7 +6,7 @@ // ========================================================================== using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; @@ -123,7 +123,32 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards }); } - public static void CanChangePlan(ChangePlan command, IAppEntity app, IAppPlansProvider appPlans) + public static Task CanTransfer(TransferToTeam command, IAppEntity app, IAppProvider appProvider, CancellationToken ct) + { + Guard.NotNull(command); + + return Validate.It(async e => + { + if (command.TeamId == null) + { + return; + } + + var team = await appProvider.GetTeamAsync(command.TeamId.Value, ct); + + if (team == null || !team.Contributors.ContainsKey(command.Actor.Identifier)) + { + e(T.Get("apps.transfer.teamNotFound")); + } + + if (app.Plan != null) + { + e(T.Get("apps.transfer.planAssigned")); + } + }); + } + + public static void CanChangePlan(ChangePlan command, IAppEntity app, IBillingPlans billingPlans) { Guard.NotNull(command); @@ -135,11 +160,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards return; } - if (appPlans.GetPlan(command.PlanId) == null) + if (billingPlans.GetPlan(command.PlanId) == null) { e(T.Get("apps.plans.notFound"), nameof(command.PlanId)); } + if (app.TeamId != null) + { + e(T.Get("apps.plans.assignedToTeam")); + } + var plan = app.Plan; if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor)) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppContributors.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppContributors.cs index 68a76a9e0..a4daee6bc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppContributors.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppContributors.cs @@ -7,7 +7,7 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { public static class GuardAppContributors { - public static Task CanAssign(AssignContributor command, IAppEntity app, IUserResolver users, IAppLimitsPlan plan) + public static Task CanAssign(AssignContributor command, IAppEntity app, IUserResolver users, Plan plan) { Guard.NotNull(command); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs index fd2c599e2..d77f8d715 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs @@ -5,9 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps { @@ -23,9 +25,13 @@ namespace Squidex.Domain.Apps.Entities.Apps string? Description { get; } + DomainId? TeamId { get; } + Roles Roles { get; } - AppPlan? Plan { get; } + AssignedPlan? Plan { get; } + + Contributors Contributors { get; } AppImage? Image { get; } @@ -33,8 +39,6 @@ namespace Squidex.Domain.Apps.Entities.Apps AppSettings Settings { get; } - AppContributors Contributors { get; } - AssetScripts AssetScripts { get; } LanguagesConfig Languages { get; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs index 8e0487e6f..1f7b79b62 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs @@ -74,6 +74,22 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes } } + public async Task> GetAppsForTeamAsync(DomainId teamId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("AppsIndex/GetAppsForTeamAsync")) + { + var apps = await appRepository.QueryAllAsync(teamId, ct); + + foreach (var app in apps.Where(IsValid)) + { + await CacheItAsync(app); + } + + return apps.Where(IsValid).ToList(); + } + } + public async Task GetAppAsync(string name, bool canCache = false, CancellationToken ct = default) { @@ -165,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes case DeleteApp delete: await OnDeleteAsync(delete); break; - case AppUpdateCommand update: + case AppCommand update: await OnUpdateAsync(update); break; } @@ -195,7 +211,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes await InvalidateItAsync(delete.AppId.Id, delete.AppId.Name); } - private async Task OnUpdateAsync(AppUpdateCommand update) + private async Task OnUpdateAsync(AppCommand update) { await InvalidateItAsync(update.AppId.Id, update.AppId.Name); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs index 15cb5163b..0422cb0a4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs @@ -15,6 +15,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes Task> GetAppsForUserAsync(string userId, PermissionSet permissions, CancellationToken ct = default); + Task> GetAppsForTeamAsync(DomainId teamId, + CancellationToken ct = default); + Task GetAppAsync(string name, bool canCache = false, CancellationToken ct = default); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEventConsumer.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEventConsumer.cs deleted file mode 100644 index 404133472..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEventConsumer.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Logging; -using NodaTime; -using Squidex.Domain.Apps.Entities.Notifications; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Apps.Invitation -{ - public sealed class InvitationEventConsumer : IEventConsumer - { - private static readonly Duration MaxAge = Duration.FromDays(2); - private readonly INotificationSender emailSender; - private readonly IUserResolver userResolver; - private readonly ILogger log; - - public string Name - { - get => "NotificationEmailSender"; - } - - public string EventsFilter - { - get { return "^app-"; } - } - - public InvitationEventConsumer(INotificationSender emailSender, IUserResolver userResolver, - ILogger log) - { - this.emailSender = emailSender; - this.userResolver = userResolver; - - this.log = log; - } - - public async Task On(Envelope @event) - { - if (!emailSender.IsActive) - { - return; - } - - if (@event.Headers.EventStreamNumber() <= 1) - { - return; - } - - var now = SystemClock.Instance.GetCurrentInstant(); - - var timestamp = @event.Headers.Timestamp(); - - if (now - timestamp > MaxAge) - { - return; - } - - if (@event.Payload is AppContributorAssigned appContributorAssigned) - { - if (!appContributorAssigned.Actor.IsUser || !appContributorAssigned.IsAdded) - { - return; - } - - var assignerId = appContributorAssigned.Actor.Identifier; - var assigneeId = appContributorAssigned.ContributorId; - - var assigner = await userResolver.FindByIdAsync(assignerId); - - if (assigner == null) - { - log.LogWarning("Failed to invite user: Assigner {assignerId} not found.", assignerId); - return; - } - - var assignee = await userResolver.FindByIdAsync(appContributorAssigned.ContributorId); - - if (assignee == null) - { - log.LogWarning("Failed to invite user: Assignee {assigneeId} not found.", assigneeId); - return; - } - - var appName = appContributorAssigned.AppId.Name; - - await emailSender.SendInviteAsync(assigner, assignee, appName); - } - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs deleted file mode 100644 index b93a27cca..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Apps.Invitation -{ - public sealed class InviteUserCommandMiddleware : ICommandMiddleware - { - private readonly IUserResolver userResolver; - - public InviteUserCommandMiddleware(IUserResolver userResolver) - { - this.userResolver = userResolver; - } - - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - if (context.Command is AssignContributor assignContributor && ShouldResolve(assignContributor)) - { - IUser? user; - - var created = false; - - if (assignContributor.Invite) - { - (user, created) = await userResolver.CreateUserIfNotExistsAsync(assignContributor.ContributorId, true, ct); - } - else - { - user = await userResolver.FindByIdOrEmailAsync(assignContributor.ContributorId, ct); - } - - if (user != null) - { - assignContributor.ContributorId = user.Id; - } - - await next(context, ct); - - if (created && context.PlainResult is IAppEntity app) - { - context.Complete(new InvitedResult { App = app }); - } - } - else - { - await next(context, ct); - } - } - - private static bool ShouldResolve(AssignContributor assignContributor) - { - return assignContributor.ContributorId.IsEmail(); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppLimitsPlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppLimitsPlan.cs deleted file mode 100644 index ea8041c9c..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppLimitsPlan.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Apps.Plans -{ - public sealed class ConfigAppLimitsPlan : IAppLimitsPlan - { - public string Id { get; set; } - - public string Name { get; set; } - - public string Costs { get; set; } - - public string? ConfirmText { get; set; } - - public string? YearlyCosts { get; set; } - - public string? YearlyId { get; set; } - - public string? YearlyConfirmText { get; set; } - - public long BlockingApiCalls { get; set; } - - public long MaxApiCalls { get; set; } - - public long MaxApiBytes { get; set; } - - public long MaxAssetSize { get; set; } - - public int MaxContributors { get; set; } - - public bool IsFree { get; set; } - - public ConfigAppLimitsPlan Clone() - { - return (ConfigAppLimitsPlan)MemberwiseClone(); - } - } -} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs deleted file mode 100644 index 377b5f10e..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Apps.Plans -{ - public sealed class ConfigAppPlansProvider : IAppPlansProvider - { - private static readonly ConfigAppLimitsPlan Infinite = new ConfigAppLimitsPlan - { - Id = "infinite", - Name = "Infinite", - MaxApiCalls = -1, - MaxAssetSize = -1, - MaxContributors = -1, - BlockingApiCalls = -1 - }; - - private readonly Dictionary plansById = new Dictionary(StringComparer.OrdinalIgnoreCase); - private readonly List plansList = new List(); - private readonly ConfigAppLimitsPlan freePlan; - - public ConfigAppPlansProvider(IEnumerable config) - { - foreach (var plan in config.OrderBy(x => x.MaxApiCalls).Select(x => x.Clone())) - { - plansList.Add(plan); - plansById[plan.Id] = plan; - - if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts)) - { - plansById[plan.YearlyId] = plan; - } - } - - freePlan = plansList.Find(x => x.IsFree) ?? Infinite; - } - - public IEnumerable GetAvailablePlans() - { - return plansList; - } - - public bool IsConfiguredPlan(string? planId) - { - return planId != null && plansById.ContainsKey(planId); - } - - public IAppLimitsPlan? GetPlan(string? planId) - { - return plansById.GetValueOrDefault(planId ?? string.Empty); - } - - public IAppLimitsPlan GetFreePlan() - { - return freePlan; - } - - public IAppLimitsPlan? GetPlanUpgradeForApp(IAppEntity app) - { - Guard.NotNull(app); - - return GetPlanUpgrade(app.Plan?.PlanId); - } - - public IAppLimitsPlan? GetPlanUpgrade(string? planId) - { - var plan = GetPlanCore(planId); - - var nextPlanIndex = plansList.IndexOf(plan); - - if (nextPlanIndex >= 0 && nextPlanIndex < plansList.Count - 1) - { - return plansList[nextPlanIndex + 1]; - } - - return null; - } - - public (IAppLimitsPlan Plan, string PlanId) GetPlanForApp(IAppEntity app) - { - Guard.NotNull(app); - - var planId = app.Plan?.PlanId; - var plan = GetPlanCore(planId); - - if (plan.YearlyId != null && plan.YearlyId == planId) - { - return (plan, plan.YearlyId); - } - else - { - return (plan, plan.Id); - } - } - - private ConfigAppLimitsPlan GetPlanCore(string? planId) - { - return plansById.GetValueOrDefault(planId ?? string.Empty) ?? freePlan; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppLimitsPlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppLimitsPlan.cs deleted file mode 100644 index 65509948f..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppLimitsPlan.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Apps.Plans -{ - public interface IAppLimitsPlan - { - string Id { get; } - - string Name { get; } - - string Costs { get; } - - string? ConfirmText { get; } - - string? YearlyCosts { get; } - - string? YearlyId { get; } - - string? YearlyConfirmText { get; } - - long BlockingApiCalls { get; } - - long MaxApiCalls { get; } - - long MaxApiBytes { get; } - - long MaxAssetSize { get; } - - int MaxContributors { get; } - } -} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppPlansProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppPlansProvider.cs deleted file mode 100644 index 1875d9cd7..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppPlansProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Apps.Plans -{ - public interface IAppPlansProvider - { - IEnumerable GetAvailablePlans(); - - bool IsConfiguredPlan(string? planId); - - IAppLimitsPlan? GetPlanUpgradeForApp(IAppEntity app); - - IAppLimitsPlan? GetPlanUpgrade(string? planId); - - IAppLimitsPlan? GetPlan(string? planId); - - IAppLimitsPlan GetFreePlan(); - - (IAppLimitsPlan Plan, string PlanId) GetPlanForApp(IAppEntity app); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs index 05973e9f2..241ea992f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs @@ -54,8 +54,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans if (context.IsCompleted && user != null) { - var newApps = totalApps + 1; - var newAppsValue = newApps.ToString(CultureInfo.InvariantCulture); + var newAppsCount = totalApps + 1; + var newAppsValue = newAppsCount.ToString(CultureInfo.InvariantCulture); // Always update the user and therefore do nto pass cancellation token. await userResolver.SetClaimAsync(user.Id, SquidexClaimTypes.TotalApps, newAppsValue, true, default); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs deleted file mode 100644 index a08eed647..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.UsageTracking; -using Squidex.Messaging; - -namespace Squidex.Domain.Apps.Entities.Apps.Plans -{ - public class UsageGate - { - private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly IAppPlansProvider appPlansProvider; - private readonly IApiUsageTracker apiUsageTracker; - private readonly IMessageBus messaging; - - public UsageGate(IAppPlansProvider appPlansProvider, IApiUsageTracker apiUsageTracker, IMessageBus messaging) - { - this.appPlansProvider = appPlansProvider; - this.apiUsageTracker = apiUsageTracker; - this.messaging = messaging; - } - - public virtual async Task IsBlockedAsync(IAppEntity app, string? clientId, DateTime today, - CancellationToken ct = default) - { - Guard.NotNull(app); - - var (plan, _) = appPlansProvider.GetPlanForApp(app); - - var appId = app.Id; - var blocking = false; - var blockLimit = plan.MaxApiCalls; - - if (blockLimit > 0 || plan.BlockingApiCalls > 0) - { - var usage = await apiUsageTracker.GetMonthCallsAsync(appId.ToString(), today, null, ct); - - if (IsOver10Percent(blockLimit, usage) && IsAboutToBeLocked(today, blockLimit, usage) && !HasNotifiedBefore(app.Id)) - { - var notification = new UsageTrackingCheck - { - AppId = appId, - AppName = app.Name, - Usage = usage, - UsageLimit = blockLimit, - Users = GetUsers(app) - }; - - await messaging.PublishAsync(notification, ct: ct); - - TrackNotified(appId); - } - - blocking = plan.BlockingApiCalls > 0 && usage > plan.BlockingApiCalls; - } - - if (!blocking) - { - if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && client.ApiCallsLimit > 0) - { - var usage = await apiUsageTracker.GetMonthCallsAsync(appId.ToString(), today, clientId, ct); - - blocking = usage >= client.ApiCallsLimit; - } - } - - return blocking; - } - - private bool HasNotifiedBefore(DomainId appId) - { - return memoryCache.Get(appId); - } - - private bool TrackNotified(DomainId appId) - { - return memoryCache.Set(appId, true, TimeSpan.FromHours(1)); - } - - private static string[] GetUsers(IAppEntity app) - { - return app.Contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToArray(); - } - - private static bool IsOver10Percent(long limit, long usage) - { - return usage > limit * 0.1; - } - - private static bool IsAboutToBeLocked(DateTime today, long limit, long usage) - { - var daysInMonth = DateTime.DaysInMonth(today.Year, today.Month); - - var forecasted = ((float)usage / today.Day) * daysInMonth; - - return forecasted > limit; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs index 09a7eaad5..4fd6ecb37 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs @@ -14,6 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Repositories Task> QueryAllAsync(string contributorId, IEnumerable names, CancellationToken ct = default); + Task> QueryAllAsync(DomainId teamId, + CancellationToken ct = default); + Task FindAsync(DomainId id, CancellationToken ct = default); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs index f08afe66e..4d63519b7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs @@ -7,23 +7,19 @@ using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure.States; -using Squidex.Infrastructure.UsageTracking; #pragma warning disable CS0649 namespace Squidex.Domain.Apps.Entities.Assets { - public partial class AssetUsageTracker : IAssetUsageTracker, IDeleter + public partial class AssetUsageTracker : IDeleter { - private const string CounterTotalCount = "TotalAssets"; - private const string CounterTotalSize = "TotalSize"; - private static readonly DateTime SummaryDate; private readonly IAssetLoader assetLoader; private readonly ISnapshotStore store; private readonly ITagService tagService; - private readonly IUsageTracker usageTracker; + private readonly IAppUsageGate appUsageGate; [CollectionName("Index_TagHistory")] public sealed class State @@ -31,13 +27,13 @@ namespace Squidex.Domain.Apps.Entities.Assets public HashSet? Tags { get; set; } } - public AssetUsageTracker(IUsageTracker usageTracker, IAssetLoader assetLoader, ITagService tagService, + public AssetUsageTracker(IAppUsageGate appUsageGate, IAssetLoader assetLoader, ITagService tagService, ISnapshotStore store) { + this.appUsageGate = appUsageGate; this.assetLoader = assetLoader; this.tagService = tagService; this.store = store; - this.usageTracker = usageTracker; ClearCache(); } @@ -45,48 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Assets Task IDeleter.DeleteAppAsync(IAppEntity app, CancellationToken ct) { - var key = GetKey(app.Id); - - return usageTracker.DeleteAsync(key, ct); - } - - public async Task GetTotalSizeAsync(DomainId appId) - { - var key = GetKey(appId); - - var counters = await usageTracker.GetAsync(key, SummaryDate, SummaryDate, null); - - return counters.GetInt64(CounterTotalSize); - } - - public async Task> QueryAsync(DomainId appId, DateTime fromDate, DateTime toDate) - { - var enriched = new List(); - - var usages = await usageTracker.QueryAsync(GetKey(appId), fromDate, toDate); - - if (usages.TryGetValue(usageTracker.FallbackCategory, out var byCategory1)) - { - AddCounters(enriched, byCategory1); - } - else if (usages.TryGetValue("Default", out var byCategory2)) - { - // Fallback for older versions where default was uses as tracking category. - AddCounters(enriched, byCategory2); - } - - return enriched; - } - - private static void AddCounters(List enriched, List<(DateTime, Counters)> details) - { - foreach (var (date, counters) in details) - { - var totalCount = counters.GetInt64(CounterTotalCount); - var totalSize = counters.GetInt64(CounterTotalSize); - - enriched.Add(new AssetStats(date, totalCount, totalSize)); - } + return appUsageGate.DeleteAssetUsageAsync(app.Id, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs index b61391b5c..10e130449 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs @@ -12,7 +12,6 @@ using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; -using Squidex.Infrastructure.UsageTracking; #pragma warning disable MA0048 // File name must match type name @@ -58,8 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await store.ClearAsync(); - // Use a well defined prefix query for the deletion to improve performance. - await usageTracker.DeleteByKeyPatternAsync("^([a-zA-Z0-9]+)_Assets"); + await appUsageGate.DeleteAssetsUsageAsync(); } public async Task On(IEnumerable> events) @@ -187,13 +185,13 @@ namespace Squidex.Domain.Apps.Entities.Assets switch (@event.Payload) { case AssetCreated assetCreated: - return UpdateSizeAsync(assetCreated.AppId.Id, GetDate(@event), assetCreated.FileSize, 1); + return appUsageGate.TrackAssetAsync(assetCreated.AppId.Id, GetDate(@event), assetCreated.FileSize, 1); case AssetUpdated assetUpdated: - return UpdateSizeAsync(assetUpdated.AppId.Id, GetDate(@event), assetUpdated.FileSize, 0); + return appUsageGate.TrackAssetAsync(assetUpdated.AppId.Id, GetDate(@event), assetUpdated.FileSize, 0); case AssetDeleted assetDeleted: - return UpdateSizeAsync(assetDeleted.AppId.Id, GetDate(@event), -assetDeleted.DeletedSize, -1); + return appUsageGate.TrackAssetAsync(assetDeleted.AppId.Id, GetDate(@event), -assetDeleted.DeletedSize, -1); } return Task.CompletedTask; @@ -203,25 +201,5 @@ namespace Squidex.Domain.Apps.Entities.Assets { return @event.Headers.Timestamp().ToDateTimeUtc().Date; } - - private Task UpdateSizeAsync(DomainId appId, DateTime date, long size, long count) - { - var counters = new Counters - { - [CounterTotalSize] = size, - [CounterTotalCount] = count - }; - - var appKey = GetKey(appId); - - return Task.WhenAll( - usageTracker.TrackAsync(date, appKey, null, counters), - usageTracker.TrackAsync(SummaryDate, appKey, null, counters)); - } - - private static string GetKey(DomainId appId) - { - return $"{appId}_Assets"; - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetCommand.cs similarity index 65% rename from backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetCommand.cs index 095855ac5..6f356ec33 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetCommand.cs @@ -8,19 +8,27 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +#pragma warning disable MA0048 // File name must match type name + namespace Squidex.Domain.Apps.Entities.Assets.Commands { - public abstract class AssetCommand : SquidexCommand, IAppCommand, IAggregateCommand + public abstract class AssetCommand : AssetCommandBase { - public NamedId AppId { get; set; } - public DomainId AssetId { get; set; } public bool DoNotScript { get; set; } - public DomainId AggregateId + public override DomainId AggregateId { get => DomainId.Combine(AppId, AssetId); } } + + // This command is needed as marker for middlewares. + public abstract class AssetCommandBase : SquidexCommand, IAppCommand, IAggregateCommand + { + public NamedId AppId { get; set; } + + public abstract DomainId AggregateId { get; } + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetFolderCommand.cs similarity index 63% rename from backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetFolderCommand.cs index f8a8d1f93..21fb707ee 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetFolderCommand.cs @@ -8,17 +8,25 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +#pragma warning disable MA0048 // File name must match type name + namespace Squidex.Domain.Apps.Entities.Assets.Commands { - public abstract class AssetFolderCommand : SquidexCommand, IAppCommand, IAggregateCommand + public abstract class AssetFolderCommand : AssetFolderCommandBase { - public NamedId AppId { get; set; } - public DomainId AssetFolderId { get; set; } - public DomainId AggregateId + public override DomainId AggregateId { get => DomainId.Combine(AppId, AssetFolderId); } } + + // This command is needed as marker for middlewares. + public abstract class AssetFolderCommandBase : SquidexCommand, IAppCommand, IAggregateCommand + { + public NamedId AppId { get; set; } + + public abstract DomainId AggregateId { get; } + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs index cb60ae55a..8d2ef9758 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs @@ -77,7 +77,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject TotalSize += e.FileSize; EnsureProperties(); - return true; } @@ -88,7 +87,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject TotalSize += e.FileSize; EnsureProperties(); - return true; } @@ -132,7 +130,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject } EnsureProperties(); - return hasChanged; } @@ -141,14 +138,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject ParentId = e.ParentId; EnsureProperties(); - return true; } case AssetDeleted: { IsDeleted = true; - return true; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs index 8b6077663..519f8f158 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs @@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject protected override bool CanAcceptCreation(ICommand command) { - return command is AssetCommand; + return command is AssetCommandBase; } protected override bool CanAccept(ICommand command) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs index 2885e480a..3bf73e437 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs @@ -41,28 +41,24 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject Id = e.AssetFolderId; SimpleMapper.Map(e, this); - return true; } case AssetFolderRenamed e when Is.OptionalChange(FolderName, e.FolderName): { FolderName = e.FolderName; - return true; } case AssetFolderMoved e when Is.Change(ParentId, e.ParentId): { ParentId = e.ParentId; - return true; } case AssetFolderDeleted: { IsDeleted = true; - return true; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs index e4c09a3d2..e1e480d33 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs @@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject protected override bool CanAcceptCreation(ICommand command) { - return command is AssetFolderCommand; + return command is AssetFolderCommandBase; } protected override bool CanAccept(ICommand command) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs index 6d1116128..56bd09cb5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs @@ -11,8 +11,16 @@ namespace Squidex.Domain.Apps.Entities.Assets { public interface IAssetUsageTracker { - Task> QueryAsync(DomainId appId, DateTime fromDate, DateTime toDate); + Task> QueryByAppAsync(DomainId appId, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); - Task GetTotalSizeAsync(DomainId appId); + Task> QueryByTeamAsync(DomainId teamId, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); + + Task GetTotalSizeByAppAsync(DomainId appId, + CancellationToken ct = default); + + Task GetTotalSizeByTeamAsync(DomainId teamId, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/ConfigPlansProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/ConfigPlansProvider.cs new file mode 100644 index 000000000..759a4f33a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/ConfigPlansProvider.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Billing +{ + public sealed class ConfigPlansProvider : IBillingPlans + { + private static readonly Plan Infinite = new Plan + { + Id = "infinite", + Name = "Infinite", + MaxApiCalls = -1, + MaxAssetSize = -1, + MaxContributors = -1, + BlockingApiCalls = -1 + }; + + private readonly Dictionary plansById = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly List plans = new List(); + private readonly Plan freePlan; + + public ConfigPlansProvider(IEnumerable config) + { + plans.AddRange(config.OrderBy(x => x.MaxApiCalls)); + + foreach (var plan in config.OrderBy(x => x.MaxApiCalls)) + { + plansById[plan.Id] = plan; + + if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts)) + { + plansById[plan.YearlyId] = plan; + } + } + + freePlan = config.FirstOrDefault(x => x.IsFree) ?? Infinite; + } + + public IEnumerable GetAvailablePlans() + { + return plans; + } + + public bool IsConfiguredPlan(string? planId) + { + return planId != null && plansById.ContainsKey(planId); + } + + public Plan? GetPlan(string? planId) + { + return plansById.GetValueOrDefault(planId ?? string.Empty); + } + + public Plan GetFreePlan() + { + return freePlan; + } + + public (Plan Plan, string PlanId) GetActualPlan(string? planId) + { + if (planId == null || !plansById.TryGetValue(planId, out var plan)) + { + var result = GetFreePlan(); + + return (result, result.Id); + } + + if (plan.YearlyId != null && plan.YearlyId == planId) + { + return (plan, plan.YearlyId); + } + else + { + return (plan, plan.Id); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/IAppUsageGate.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/IAppUsageGate.cs new file mode 100644 index 000000000..4a481d741 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/IAppUsageGate.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Billing +{ + public interface IAppUsageGate + { + Task IsBlockedAsync(IAppEntity app, string? clientId, DateTime date, + CancellationToken ct = default); + + Task TrackRequestAsync(IAppEntity app, string? clientId, DateTime date, double costs, long elapsedMs, long bytes, + CancellationToken ct = default); + + Task TrackAssetAsync(DomainId appId, DateTime date, long fileSize, long count, + CancellationToken ct = default); + + Task DeleteAssetUsageAsync(DomainId appId, + CancellationToken ct = default); + + Task DeleteAssetsUsageAsync( + CancellationToken ct = default); + + Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(IAppEntity app, + CancellationToken ct = default); + + Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(DomainId appId, + CancellationToken ct = default); + + Task<(Plan Plan, string PlanId)> GetPlanForTeamAsync(ITeamEntity team, + CancellationToken ct = default); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppPlanBillingManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingManager.cs similarity index 67% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppPlanBillingManager.cs rename to backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingManager.cs index 963f935a5..84cca89ad 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppPlanBillingManager.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingManager.cs @@ -7,21 +7,30 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Plans +namespace Squidex.Domain.Apps.Entities.Billing { - public interface IAppPlanBillingManager + public interface IBillingManager { bool HasPortal { get; } Task MustRedirectToPortalAsync(string userId, NamedId appId, string? planId, CancellationToken ct = default); + Task MustRedirectToPortalAsync(string userId, DomainId teamId, string? planId, + CancellationToken ct = default); + Task SubscribeAsync(string userId, NamedId appId, string planId, CancellationToken ct = default); + Task SubscribeAsync(string userId, DomainId teamId, string planId, + CancellationToken ct = default); + Task UnsubscribeAsync(string userId, NamedId appId, CancellationToken ct = default); + Task UnsubscribeAsync(string userId, DomainId teamId, + CancellationToken ct = default); + Task GetPortalLinkAsync(string userId, CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingPlans.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingPlans.cs new file mode 100644 index 000000000..00abfd961 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingPlans.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Billing +{ + public interface IBillingPlans + { + IEnumerable GetAvailablePlans(); + + bool IsConfiguredPlan(string? planId); + + Plan? GetPlan(string? planId); + + Plan GetFreePlan(); + + (Plan Plan, string PlanId) GetActualPlan(string? planId); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/Messages.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/Messages.cs similarity index 93% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/Messages.cs rename to backend/src/Squidex.Domain.Apps.Entities/Billing/Messages.cs index 8055333b0..79f3017f9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/Messages.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/Messages.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure; #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Apps.Plans +namespace Squidex.Domain.Apps.Entities.Billing { public sealed record UsageTrackingCheck { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/NoopAppPlanBillingManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/NoopBillingManager.cs similarity index 65% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/NoopAppPlanBillingManager.cs rename to backend/src/Squidex.Domain.Apps.Entities/Billing/NoopBillingManager.cs index 2a3d22307..30a034163 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/NoopAppPlanBillingManager.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/NoopBillingManager.cs @@ -7,9 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Plans +namespace Squidex.Domain.Apps.Entities.Billing { - public sealed class NoopAppPlanBillingManager : IAppPlanBillingManager + public sealed class NoopBillingManager : IBillingManager { public bool HasPortal { @@ -28,16 +28,34 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans return Task.FromResult(null); } + public Task MustRedirectToPortalAsync(string userId, DomainId teamId, string? planId, + CancellationToken ct = default) + { + return Task.FromResult(null); + } + public Task SubscribeAsync(string userId, NamedId appId, string planId, CancellationToken ct = default) { return Task.CompletedTask; } + public Task SubscribeAsync(string userId, DomainId teamId, string planId, + CancellationToken ct = default) + { + return Task.CompletedTask; + } + public Task UnsubscribeAsync(string userId, NamedId appId, CancellationToken ct = default) { return Task.CompletedTask; } + + public Task UnsubscribeAsync(string userId, DomainId teamId, + CancellationToken ct = default) + { + return Task.CompletedTask; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/Plan.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/Plan.cs new file mode 100644 index 000000000..a12227b11 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/Plan.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Billing +{ + public sealed record Plan + { + public string Id { get; init; } + + public string Name { get; init; } + + public string Costs { get; init; } + + public string? ConfirmText { get; init; } + + public string? YearlyCosts { get; init; } + + public string? YearlyId { get; init; } + + public string? YearlyConfirmText { get; init; } + + public long BlockingApiCalls { get; init; } + + public long MaxApiCalls { get; init; } + + public long MaxApiBytes { get; init; } + + public long MaxAssetSize { get; init; } + + public long MaxContributors { get; init; } + + public bool IsFree { get; init; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/PlanChangedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/PlanChangedResult.cs similarity index 92% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/PlanChangedResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Billing/PlanChangedResult.cs index bbe78e3d3..bd7d4c936 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/PlanChangedResult.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/PlanChangedResult.cs @@ -7,7 +7,7 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Apps.Plans +namespace Squidex.Domain.Apps.Entities.Billing { public sealed record PlanChangedResult(string PlanId, bool Unsubscribed = false, Uri? RedirectUri = null) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs new file mode 100644 index 000000000..cc6ebab97 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs @@ -0,0 +1,315 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Infrastructure; +using Squidex.Infrastructure.UsageTracking; +using Squidex.Messaging; + +namespace Squidex.Domain.Apps.Entities.Billing +{ + public sealed class UsageGate : IAppUsageGate, IAssetUsageTracker + { + private const string CounterTotalCount = "TotalAssets"; + private const string CounterTotalSize = "TotalSize"; + private static readonly DateTime SummaryDate = default; + private readonly IBillingPlans billingPlans; + private readonly IAppProvider appProvider; + private readonly IApiUsageTracker apiUsageTracker; + private readonly IMemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly IMessageBus messaging; + private readonly IUsageTracker usageTracker; + + public UsageGate( + IAppProvider appProvider, + IApiUsageTracker apiUsageTracker, + IBillingPlans billingPlans, + IMessageBus messaging, + IUsageTracker usageTracker) + { + this.appProvider = appProvider; + this.apiUsageTracker = apiUsageTracker; + this.billingPlans = billingPlans; + this.messaging = messaging; + this.usageTracker = usageTracker; + } + + public Task DeleteAssetUsageAsync(DomainId appId, + CancellationToken ct = default) + { + // Do not delete the team, as this is only called when an app is deleted. + return usageTracker.DeleteAsync(AppAssetsKey(appId), ct); + } + + public Task DeleteAssetsUsageAsync( + CancellationToken ct = default) + { + // Use a well defined prefix query for the deletion to improve performance. + return usageTracker.DeleteByKeyPatternAsync("^([a-zA-Z0-9]+)_Assets", ct); + } + + public Task GetTotalSizeByAppAsync(DomainId appId, + CancellationToken ct = default) + { + return GetTotalSizeAsync(AppAssetsKey(appId), ct); + } + + public Task GetTotalSizeByTeamAsync(DomainId teamId, + CancellationToken ct = default) + { + return GetTotalSizeAsync(TeamAssetsKey(teamId), ct); + } + + private async Task GetTotalSizeAsync(string key, + CancellationToken ct) + { + var counters = await usageTracker.GetAsync(key, SummaryDate, SummaryDate, null, ct); + + return counters.GetInt64(CounterTotalSize); + } + + public Task> QueryByAppAsync(DomainId appId, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) + { + return QueryAsync(AppAssetsKey(appId), fromDate, toDate, ct); + } + + public Task> QueryByTeamAsync(DomainId teamId, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) + { + return QueryAsync(TeamAssetsKey(teamId), fromDate, toDate, ct); + } + + private async Task> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct) + { + var enriched = new List(); + + var usages = await usageTracker.QueryAsync(key, fromDate, toDate, ct); + + if (usages.TryGetValue(usageTracker.FallbackCategory, out var byCategory1)) + { + AddCounters(enriched, byCategory1); + } + + return enriched; + } + + private static void AddCounters(List enriched, List<(DateTime, Counters)> details) + { + foreach (var (date, counters) in details) + { + var totalCount = counters.GetInt64(CounterTotalCount); + var totalSize = counters.GetInt64(CounterTotalSize); + + enriched.Add(new AssetStats(date, totalCount, totalSize)); + } + } + + public async Task TrackRequestAsync(IAppEntity app, string? clientId, DateTime date, double costs, long elapsedMs, long bytes, + CancellationToken ct = default) + { + var appId = app.Id.ToString(); + + if (app.TeamId != null) + { + await apiUsageTracker.TrackAsync(date, app.TeamId.ToString()!, app.Name, costs, elapsedMs, bytes, ct); + } + + await apiUsageTracker.TrackAsync(date, appId, clientId, costs, elapsedMs, bytes, ct); + } + + public async Task IsBlockedAsync(IAppEntity app, string? clientId, DateTime date, + CancellationToken ct = default) + { + Guard.NotNull(app); + + // Resolve the plan from either the app or the assigned team. + var (plan, _, teamId) = await GetPlanForAppAsync(app, ct); + + var appId = app.Id; + var blocking = false; + var blockLimit = plan.MaxApiCalls; + var referenceId = teamId ?? app.Id; + + if (blockLimit > 0 || plan.BlockingApiCalls > 0) + { + var usage = await apiUsageTracker.GetMonthCallsAsync(referenceId.ToString(), date, null, ct); + + if (IsOver10Percent(blockLimit, usage) && IsAboutToBeLocked(date, blockLimit, usage) && !HasNotifiedBefore(appId)) + { + var notification = new UsageTrackingCheck + { + AppId = appId, + AppName = app.Name, + Usage = usage, + UsageLimit = blockLimit, + Users = GetUsers(app) + }; + + await messaging.PublishAsync(notification, ct: ct); + + TrackNotified(appId); + } + + blocking = plan.BlockingApiCalls > 0 && usage > plan.BlockingApiCalls; + } + + if (!blocking) + { + if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && client.ApiCallsLimit > 0) + { + var usage = await apiUsageTracker.GetMonthCallsAsync(appId.ToString(), date, clientId, ct); + + blocking = usage >= client.ApiCallsLimit; + } + } + + return blocking; + } + + private bool HasNotifiedBefore(DomainId appId) + { + return memoryCache.Get(appId); + } + + private bool TrackNotified(DomainId appId) + { + return memoryCache.Set(appId, true, TimeSpan.FromHours(1)); + } + + private static string[] GetUsers(IAppEntity app) + { + return app.Contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToArray(); + } + + private static bool IsOver10Percent(long limit, long usage) + { + return usage > limit * 0.1; + } + + private static bool IsAboutToBeLocked(DateTime today, long limit, long usage) + { + var daysInMonth = DateTime.DaysInMonth(today.Year, today.Month); + + var forecasted = ((float)usage / today.Day) * daysInMonth; + + return forecasted > limit; + } + + public async Task TrackAssetAsync(DomainId appId, DateTime date, long fileSize, long count, + CancellationToken ct = default) + { + var counters = new Counters + { + [CounterTotalSize] = fileSize, + [CounterTotalCount] = count + }; + + var appKey = AppAssetsKey(appId); + + var tasks = new List + { + usageTracker.TrackAsync(date, appKey, null, counters, ct), + usageTracker.TrackAsync(SummaryDate, appKey, null, counters, ct) + }; + + var (_, _, teamId) = await GetPlanForAppAsync(appId, ct); + + if (teamId != null) + { + var teamKey = TeamAssetsKey(teamId.Value); + + tasks.Add(usageTracker.TrackAsync(date, teamKey, null, counters, ct)); + tasks.Add(usageTracker.TrackAsync(SummaryDate, teamKey, null, counters, ct)); + } + + await Task.WhenAll(tasks); + } + + public Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(IAppEntity app, + CancellationToken ct = default) + { + Guard.NotNull(app); + + return memoryCache.GetOrCreateAsync(app, async x => + { + x.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); + + return await GetPlanCoreAsync(app, ct); + }); + } + + public Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(DomainId appId, + CancellationToken ct = default) + { + return memoryCache.GetOrCreateAsync(appId, async x => + { + x.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); + + return await GetPlanCoreAsync(appId, ct); + }); + } + + private async Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanCoreAsync(DomainId appId, + CancellationToken ct) + { + var app = await appProvider.GetAppAsync(appId, true, ct); + + if (app == null) + { + var freePlan = billingPlans.GetFreePlan(); + + return (freePlan, freePlan.Id, null); + } + + return await GetPlanCoreAsync(app, ct); + } + + private async Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanCoreAsync(IAppEntity app, + CancellationToken ct) + { + if (app.TeamId != null) + { + var team = await appProvider.GetTeamAsync(app.TeamId.Value, ct); + + var (plan, planId) = billingPlans.GetActualPlan(team?.Plan?.PlanId ?? app.Plan?.PlanId); + + return (plan, planId, team?.Id); + } + else + { + var (plan, planId) = billingPlans.GetActualPlan(app.Plan?.PlanId); + + return (plan, planId, null); + } + } + + public Task<(Plan Plan, string PlanId)> GetPlanForTeamAsync(ITeamEntity team, + CancellationToken ct = default) + { + var (plan, planId) = billingPlans.GetActualPlan(team?.Plan?.PlanId); + + return Task.FromResult((plan, planId)); + } + + private static string AppAssetsKey(DomainId appId) + { + return $"{appId}_Assets"; + } + + private static string TeamAssetsKey(DomainId appId) + { + return $"{appId}_TeamAssets"; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageNotifierWorker.cs similarity index 98% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs rename to backend/src/Squidex.Domain.Apps.Entities/Billing/UsageNotifierWorker.cs index 8fb735879..4f95e7033 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageNotifierWorker.cs @@ -13,7 +13,7 @@ using Squidex.Infrastructure.Tasks; using Squidex.Messaging; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Apps.Plans +namespace Squidex.Domain.Apps.Entities.Billing { public sealed class UsageNotifierWorker : IMessageHandler { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs index de9c66b09..13565080a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Commands { - public abstract class CommentTextCommand : CommentsCommand + public abstract class CommentTextCommand : CommentCommand { public string Text { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs index 756f41c0c..0db887bf9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Commands { - public sealed class DeleteComment : CommentsCommand + public sealed class DeleteComment : CommentCommand { } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/_CommentsCommand.cs similarity index 51% rename from backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/_CommentsCommand.cs index 089ed024a..dabb7d7fe 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/_CommentsCommand.cs @@ -8,21 +8,42 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +#pragma warning disable MA0048 // File name must match type name + namespace Squidex.Domain.Apps.Entities.Comments.Commands { - public abstract class CommentsCommand : SquidexCommand, IAppCommand, IAggregateCommand + public abstract class CommentCommand : CommentsCommand { - public static readonly NamedId NoApp = NamedId.Of(DomainId.NewGuid(), "none"); + public DomainId CommentId { get; set; } + } - public NamedId AppId { get; set; } + public abstract class CommentsCommand : CommentsCommandBase + { + public static readonly NamedId NoApp = NamedId.Of(DomainId.NewGuid(), "none"); public DomainId CommentsId { get; set; } - public DomainId CommentId { get; set; } - - DomainId IAggregateCommand.AggregateId + public override DomainId AggregateId { - get => AppId.Id != default ? DomainId.Combine(AppId.Id, CommentsId) : CommentsId; + get + { + if (AppId.Id == default) + { + return CommentsId; + } + else + { + return DomainId.Combine(AppId, CommentsId); + } + } } } + + // This command is needed as marker for middlewares. + public abstract class CommentsCommandBase : SquidexCommand, IAppCommand, IAggregateCommand + { + public NamedId AppId { get; set; } + + public abstract DomainId AggregateId { get; } + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs index bf3b746ef..6331c4f8d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs @@ -12,7 +12,7 @@ using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { - public sealed class CommentsCommandMiddleware : AggregateCommandMiddleware + public sealed class CommentsCommandMiddleware : AggregateCommandMiddleware { private static readonly Regex MentionRegex = new Regex(@"@(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*", RegexOptions.Compiled | RegexOptions.ExplicitCapture, TimeSpan.FromMilliseconds(100)); private readonly IUserResolver userResolver; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/_ContentCommand.cs similarity index 69% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/_ContentCommand.cs index 89ba1fb61..33f40a4de 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/_ContentCommand.cs @@ -8,21 +8,28 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +#pragma warning disable MA0048 // File name must match type name + namespace Squidex.Domain.Apps.Entities.Contents.Commands { - public abstract class ContentCommand : SquidexCommand, IAppCommand, ISchemaCommand, IAggregateCommand + public abstract class ContentCommand : ContentCommandBase { - public NamedId AppId { get; set; } - - public NamedId SchemaId { get; set; } - public DomainId ContentId { get; set; } public bool DoNotScript { get; set; } - public DomainId AggregateId + public override DomainId AggregateId { get => DomainId.Combine(AppId, ContentId); } } + + public abstract class ContentCommandBase : SquidexCommand, IAppCommand, ISchemaCommand, IAggregateCommand + { + public NamedId AppId { get; set; } + + public NamedId SchemaId { get; set; } + + public abstract DomainId AggregateId { get; } + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs index 63b6302f5..5bf4ee03c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject protected override bool CanAcceptCreation(ICommand command) { - return command is CreateContent or UpsertContent; + return command is ContentCommandBase; } protected override bool CanRecreate() diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs index bc4dd77e7..4634fe85f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs @@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Subscriptions; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; -using Squidex.Messaging.Subscriptions; using Squidex.Shared; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets @@ -53,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets new QueryArgument(Scalars.NonNullString) { Name = "id", - Description = "The id of the asset (usually GUID).", + Description = "The ID of the asset (usually GUID).", DefaultValue = null } }; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index 26d95fa30..6b76a5ceb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -444,7 +444,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents new QueryArgument(Scalars.NonNullString) { Name = "id", - Description = "The id of the content (usually GUID).", + Description = "The ID of the content (usually GUID).", DefaultValue = null }, new QueryArgument(Scalars.Int) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index a1490c282..505472e31 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries if (!HasPermission(context, schema, PermissionIds.AppContentsRead)) { - q = q with { CreatedBy = context.User.Token() }; + q = q with { CreatedBy = context.UserPrincipal.Token() }; } q = await queryParser.ParseAsync(context, q, schema); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs index 737476ecd..f56c38248 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps } else { - content.NextStatuses = await contentWorkflow.GetNextAsync(content, editingStatus, context.User); + content.NextStatuses = await contentWorkflow.GetNextAsync(content, editingStatus, context.UserPrincipal); } } @@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps { var editingStatus = content.NewStatus ?? content.Status; - content.CanUpdate = await contentWorkflow.CanUpdateAsync(content, editingStatus, context.User); + content.CanUpdate = await contentWorkflow.CanUpdateAsync(content, editingStatus, context.UserPrincipal); } private async Task EnrichColorAsync(ContentEntity content, ContentEntity result, Dictionary<(DomainId, Status), StatusInfo> cache) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs index c12bb3bc1..8cf20b80c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps AppName = schema.AppId.Name, SchemaId = schema.Id, SchemaName = schema.SchemaDef.Name, - User = context.User + User = context.UserPrincipal }; var preScript = schema.SchemaDef.Scripts.QueryPre; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs index c73e68414..290c06562 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs @@ -362,7 +362,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text var ids = events .Select(x => x.Payload).OfType() - .Select(x => DomainId.Combine(x.AppId.Id, x.ContentId)) + .Select(x => DomainId.Combine(x.AppId, x.ContentId)) .ToHashSet(); return textIndexerState.GetAsync(ids); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Context.cs b/backend/src/Squidex.Domain.Apps.Entities/Context.cs index 1a1ef2c10..f64b814e3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Context.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Context.cs @@ -25,11 +25,11 @@ namespace Squidex.Domain.Apps.Entities public ClaimsPermissions UserPermissions { get; } - public ClaimsPrincipal User { get; } + public ClaimsPrincipal UserPrincipal { get; } public IAppEntity App { get; set; } - public bool IsFrontendClient => User.IsInClient(DefaultClients.Frontend); + public bool IsFrontendClient => UserPrincipal.IsInClient(DefaultClients.Frontend); public Context(ClaimsPrincipal user, IAppEntity app) : this(app, user, user.Claims.Permissions(), EmptyHeaders) @@ -37,11 +37,15 @@ namespace Squidex.Domain.Apps.Entities Guard.NotNull(user); } - private Context(IAppEntity app, ClaimsPrincipal user, ClaimsPermissions userPermissions, IReadOnlyDictionary headers) + private Context( + IAppEntity app, + ClaimsPrincipal userPrincipal, + ClaimsPermissions userPermissions, + IReadOnlyDictionary headers) { App = app; - User = user; + UserPrincipal = userPrincipal; UserPermissions = userPermissions; Headers = headers; @@ -84,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities { if (headers != null) { - return new Context(context.App!, context.User, context.UserPermissions, headers); + return new Context(context.App!, context.UserPrincipal, context.UserPermissions, headers); } return context; diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs index d3cfacf7d..f9ae6ef90 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.History { public DomainId Id { get; set; } = DomainId.NewGuid(); - public DomainId AppId { get; set; } + public DomainId OwnerId { get; set; } public Instant Created { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs index e909556ba..b0647cdb2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs @@ -7,6 +7,7 @@ using Squidex.Domain.Apps.Entities.History.Repositories; using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Teams; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; @@ -34,9 +35,12 @@ namespace Squidex.Domain.Apps.Entities.History get => GetType().Name; } - public HistoryService(IHistoryEventRepository repository, IEnumerable creators, NotifoService notifo) + public HistoryService(IHistoryEventRepository repository, IEnumerable creators, + NotifoService notifo) { this.creators = creators.ToList(); + this.repository = repository; + this.notifo = notifo; foreach (var creator in this.creators) { @@ -46,9 +50,6 @@ namespace Squidex.Domain.Apps.Entities.History } } - this.repository = repository; - - this.notifo = notifo; } public Task ClearAsync() @@ -58,32 +59,35 @@ namespace Squidex.Domain.Apps.Entities.History public async Task On(IEnumerable> events) { - var targets = new List<(Envelope Event, HistoryEvent? HistoryEvent)>(); + var targets = new List<(Envelope Event, HistoryEvent? HistoryEvent)>(); foreach (var @event in events) { - if (@event.Payload is AppEvent) + switch (@event.Payload) { - var appEvent = @event.To(); + case AppEvent appEvent: + { + var historyEvent = await CreateEvent(appEvent.AppId.Id, appEvent.Actor, @event); - HistoryEvent? historyEvent = null; + if (historyEvent != null) + { + targets.Add((@event, historyEvent)); + } - foreach (var creator in creators) - { - historyEvent = await creator.CreateEventAsync(@event); + break; + } - if (historyEvent != null) + case TeamEvent teamEvent: { - historyEvent.Actor = appEvent.Payload.Actor; - historyEvent.AppId = appEvent.Payload.AppId.Id; - historyEvent.Created = @event.Headers.Timestamp(); - historyEvent.Version = @event.Headers.EventStreamNumber(); + var historyEvent = await CreateEvent(teamEvent.TeamId, teamEvent.Actor, @event); + + if (historyEvent != null) + { + targets.Add((@event, historyEvent)); + } break; } - } - - targets.Add((appEvent, historyEvent)); } } @@ -95,10 +99,29 @@ namespace Squidex.Domain.Apps.Entities.History } } - public async Task> QueryByChannelAsync(DomainId appId, string channelPrefix, int count, + private async Task CreateEvent(DomainId ownerId, RefToken actor, Envelope @event) + { + foreach (var creator in creators) + { + var historyEvent = await creator.CreateEventAsync(@event); + + if (historyEvent != null) + { + historyEvent.Actor = actor; + historyEvent.OwnerId = ownerId; + historyEvent.Created = @event.Headers.Timestamp(); + historyEvent.Version = @event.Headers.EventStreamNumber(); + return historyEvent; + } + } + + return null; + } + + public async Task> QueryByChannelAsync(DomainId ownerId, string channelPrefix, int count, CancellationToken ct = default) { - var items = await repository.QueryByChannelAsync(appId, channelPrefix, count, ct); + var items = await repository.QueryByChannelAsync(ownerId, channelPrefix, count, ct); return items.Select(x => new ParsedHistoryEvent(x, texts)).ToList(); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs index 1070e10b3..48f90ef67 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs @@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Entities.History { public interface IHistoryService { - Task> QueryByChannelAsync(DomainId appId, string channelPrefix, int count, + Task> QueryByChannelAsync(DomainId ownerId, string channelPrefix, int count, CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs index 4ca364af5..d69bb7c3f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs @@ -14,6 +14,7 @@ using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Comments; using Squidex.Domain.Apps.Events.Contents; +using Squidex.Domain.Apps.Events.Teams; using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; @@ -141,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.History } } - public async Task HandleEventsAsync(IEnumerable<(Envelope AppEvent, HistoryEvent? HistoryEvent)> events) + public async Task HandleEventsAsync(IEnumerable<(Envelope AppEvent, HistoryEvent? HistoryEvent)> events) { Guard.NotNull(events); @@ -176,12 +177,20 @@ namespace Squidex.Domain.Apps.Entities.History { switch (@event.AppEvent.Payload) { - case AppContributorAssigned contributorAssigned: - await AssignContributorAsync(client, contributorAssigned); + case AppContributorAssigned assigned: + await AssignContributorAsync(client, assigned.ContributorId, GetAppPrefix(assigned)); break; - case AppContributorRemoved contributorRemoved: - await RemoveContributorAsync(client, contributorRemoved); + case AppContributorRemoved removed: + await RemoveContributorAsync(client, removed.ContributorId, GetAppPrefix(removed)); + break; + + case TeamContributorAssigned assigned: + await AssignContributorAsync(client, assigned.ContributorId, GetTeamPrefix(assigned)); + break; + + case TeamContributorRemoved removed: + await RemoveContributorAsync(client, removed.ContributorId, GetTeamPrefix(removed)); break; } } @@ -196,10 +205,8 @@ namespace Squidex.Domain.Apps.Entities.History } } - private async Task AssignContributorAsync(INotifoClient actualClient, AppContributorAssigned contributorAssigned) + private async Task AssignContributorAsync(INotifoClient actualClient, string userId, string prefix) { - var userId = contributorAssigned.ContributorId; - var user = await userResolver.FindByIdAsync(userId); if (user != null) @@ -211,7 +218,7 @@ namespace Squidex.Domain.Apps.Entities.History { var request = new AddAllowedTopicDto { - Prefix = GetAppPrefix(contributorAssigned) + Prefix = prefix }; await actualClient.Users.PostAllowedTopicAsync(options.AppId, userId, request); @@ -222,14 +229,10 @@ namespace Squidex.Domain.Apps.Entities.History } } - private async Task RemoveContributorAsync(INotifoClient actualClient, AppContributorRemoved contributorRemoved) + private async Task RemoveContributorAsync(INotifoClient actualClient, string userId, string prefix) { - var userId = contributorRemoved.ContributorId; - try { - var prefix = GetAppPrefix(contributorRemoved); - await actualClient.Users.DeleteAllowedTopicAsync(options.ApiKey, userId, prefix); } catch (NotifoException ex) when (ex.StatusCode != 404) @@ -238,22 +241,22 @@ namespace Squidex.Domain.Apps.Entities.History } } - private IEnumerable CreateRequests(Envelope appEvent, HistoryEvent? historyEvent) + private IEnumerable CreateRequests(Envelope @event, HistoryEvent? historyEvent) { - if (appEvent.Payload is CommentCreated { Mentions.Length: > 0 } comment) + if (@event.Payload is CommentCreated { Mentions.Length: > 0 } comment) { foreach (var userId in comment.Mentions) { yield return CreateMentionRequest(comment, userId); } } - else if (historyEvent != null) + else if (historyEvent != null && @event.Payload is AppEvent appEvent) { - yield return CreateHistoryRequest(historyEvent, appEvent.Payload); + yield return CreateHistoryRequest(historyEvent, appEvent); } } - private PublishDto CreateHistoryRequest(HistoryEvent historyEvent, AppEvent payload) + private PublishDto CreateHistoryRequest(HistoryEvent historyEvent, IEvent payload) { var publishRequest = new PublishDto { @@ -265,7 +268,25 @@ namespace Squidex.Domain.Apps.Entities.History publishRequest.Properties.Add(key, value); } - publishRequest.Properties["SquidexApp"] = payload.AppId.Name; + if (payload is AppEvent appEvent) + { + publishRequest.Properties["SquidexApp"] = appEvent.AppId.Name; + } + + if (payload is SquidexEvent squidexEvent) + { + SetUser(squidexEvent, publishRequest); + } + + if (payload is AppEvent appEvent2) + { + publishRequest.Topic = BuildTopic(GetAppPrefix(appEvent2), historyEvent); + } + + if (payload is TeamEvent teamEvent) + { + publishRequest.Topic = BuildTopic(GetTeamPrefix(teamEvent), historyEvent); + } if (payload is ContentEvent @event and not ContentDeleted) { @@ -276,9 +297,6 @@ namespace Squidex.Domain.Apps.Entities.History publishRequest.TemplateCode = historyEvent.EventType; - SetUser(payload, publishRequest); - SetTopic(payload, publishRequest, historyEvent); - return publishRequest; } @@ -309,25 +327,27 @@ namespace Squidex.Domain.Apps.Entities.History return publishRequest; } - private static void SetUser(AppEvent appEvent, PublishDto publishRequest) + private static void SetUser(SquidexEvent @event, PublishDto publishRequest) { - if (appEvent.Actor.IsUser) + if (@event.Actor.IsUser) { - publishRequest.CreatorId = appEvent.Actor.Identifier; + publishRequest.CreatorId = @event.Actor.Identifier; } } - private static void SetTopic(AppEvent appEvent, PublishDto publishRequest, HistoryEvent @event) + private static string BuildTopic(string prefix, HistoryEvent @event) { - var topicPrefix = GetAppPrefix(appEvent); - var topicSuffix = @event.Channel.Replace('.', '/').Trim(); - - publishRequest.Topic = $"{topicPrefix}/{topicSuffix}"; + return $"{prefix}/{@event.Channel.Replace('.', '/').Trim()}"; } private static string GetAppPrefix(AppEvent appEvent) { return $"apps/{appEvent.AppId.Id}"; } + + private static string GetTeamPrefix(TeamEvent teamEvent) + { + return $"apps/{teamEvent.TeamId}"; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs index fd1f09be2..db6b2283a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs @@ -8,6 +8,7 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Teams; using Squidex.Infrastructure; using Squidex.Infrastructure.Security; @@ -18,6 +19,12 @@ namespace Squidex.Domain.Apps.Entities Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(DomainId appId, DomainId id, bool canCache = false, CancellationToken ct = default); + Task GetTeamAsync(DomainId teamId, + CancellationToken ct = default); + + Task> GetUserTeamsAsync(string userId, + CancellationToken ct = default); + Task GetAppAsync(DomainId appId, bool canCache = false, CancellationToken ct = default); @@ -27,6 +34,9 @@ namespace Squidex.Domain.Apps.Entities Task> GetUserAppsAsync(string userId, PermissionSet permissions, CancellationToken ct = default); + Task> GetTeamAppsAsync(DomainId teamId, + CancellationToken ct = default); + Task GetSchemaAsync(DomainId appId, DomainId id, bool canCache = false, CancellationToken ct = default); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/ITeamCommand.cs similarity index 64% rename from backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/ITeamCommand.cs index 2f31e7da0..07cfdf672 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/ITeamCommand.cs @@ -8,12 +8,10 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities { - public abstract class SchemaCommand : SquidexCommand, IAppCommand, IAggregateCommand + public interface ITeamCommand : ICommand { - public NamedId AppId { get; set; } - - public abstract DomainId AggregateId { get; } + DomainId TeamId { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs new file mode 100644 index 000000000..969b011a2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs @@ -0,0 +1,131 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Logging; +using NodaTime; +using Squidex.Domain.Apps.Entities.Notifications; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Teams; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Invitation +{ + public sealed class InvitationEventConsumer : IEventConsumer + { + private static readonly Duration MaxAge = Duration.FromDays(2); + private readonly INotificationSender emailSender; + private readonly IUserResolver userResolver; + private readonly IAppProvider appProvider; + private readonly ILogger log; + + public string Name + { + get => "NotificationEmailSender"; + } + + public string EventsFilter + { + get { return "^app-|^app-"; } + } + + public InvitationEventConsumer(INotificationSender emailSender, IUserResolver userResolver, IAppProvider appProvider, + ILogger log) + { + this.emailSender = emailSender; + this.userResolver = userResolver; + this.appProvider = appProvider; + this.log = log; + } + + public async Task On(Envelope @event) + { + if (!emailSender.IsActive) + { + return; + } + + if (@event.Headers.EventStreamNumber() <= 1) + { + return; + } + + var now = SystemClock.Instance.GetCurrentInstant(); + + var timestamp = @event.Headers.Timestamp(); + + if (now - timestamp > MaxAge) + { + return; + } + + switch (@event.Payload) + { + case AppContributorAssigned assigned when assigned.IsAdded: + { + var (assigner, assignee) = await ResolveUsersAsync(assigned.Actor, assigned.ContributorId, default); + + if (assigner == null || assignee == null) + { + return; + } + + await emailSender.SendInviteAsync(assigner, assignee, assigned.AppId.Name); + return; + } + + case TeamContributorAssigned assigned when assigned.IsAdded: + { + var (assigner, assignee) = await ResolveUsersAsync(assigned.Actor, assigned.ContributorId, default); + + if (assigner == null || assignee == null) + { + return; + } + + var team = await appProvider.GetTeamAsync(assigned.TeamId); + + if (team == null) + { + return; + } + + await emailSender.SendTeamInviteAsync(assigner, assignee, team.Name); + break; + } + } + } + + private async Task<(IUser? Assignee, IUser? Assigner)> ResolveUsersAsync(RefToken assignerId, string assigneeId, + CancellationToken ct) + { + if (!assignerId.IsUser) + { + return default; + } + + var assigner = await userResolver.FindByIdAsync(assignerId.Identifier, ct); + + if (assigner == null) + { + log.LogWarning("Failed to invite user: Assigner {assignerId} not found.", assignerId); + return default; + } + + var assignee = await userResolver.FindByIdAsync(assigneeId, ct); + + if (assignee == null) + { + log.LogWarning("Failed to invite user: Assignee {assigneeId} not found.", assigneeId); + return default; + } + + return (assigner, assignee); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Invitation/InviteUserCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InviteUserCommandMiddleware.cs new file mode 100644 index 000000000..8d4065a2b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InviteUserCommandMiddleware.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Shared.Users; +using AssignAppContributor = Squidex.Domain.Apps.Entities.Apps.Commands.AssignContributor; +using AssignTeamContributor = Squidex.Domain.Apps.Entities.Teams.Commands.AssignContributor; + +namespace Squidex.Domain.Apps.Entities.Invitation +{ + public sealed class InviteUserCommandMiddleware : ICommandMiddleware + { + private readonly IUserResolver userResolver; + + public InviteUserCommandMiddleware(IUserResolver userResolver) + { + this.userResolver = userResolver; + } + + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (context.Command is AssignAppContributor assignAppContributor) + { + var (userId, created) = + await ResolveUserAsync( + assignAppContributor.ContributorId, + assignAppContributor.Invite, + ct); + + assignAppContributor.ContributorId = userId; + + await next(context, ct); + + if (created && context.PlainResult is IAppEntity app) + { + context.Complete(new InvitedResult { Entity = app }); + } + } + else if (context.Command is AssignTeamContributor assignTeamContributor) + { + var (userId, created) = + await ResolveUserAsync( + assignTeamContributor.ContributorId, + assignTeamContributor.Invite, + ct); + + assignTeamContributor.ContributorId = userId; + + await next(context, ct); + + if (created && context.PlainResult is ITeamEntity team) + { + context.Complete(new InvitedResult { Entity = team }); + } + } + else + { + await next(context, ct); + } + } + + private async Task<(string Id, bool)> ResolveUserAsync(string id, bool invite, + CancellationToken ct) + { + if (!id.IsEmail()) + { + return (id, false); + } + + if (invite) + { + var (createdUser, created) = await userResolver.CreateUserIfNotExistsAsync(id, true, ct); + + return (createdUser?.Id ?? id, created); + } + + var user = await userResolver.FindByIdOrEmailAsync(id, ct); + + return (user?.Id ?? id, false); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitedResult.cs similarity index 73% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitedResult.cs index 45c6df7b9..120dc10b7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitedResult.cs @@ -5,10 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Invitation +namespace Squidex.Domain.Apps.Entities.Invitation { - public sealed class InvitedResult + public sealed class InvitedResult { - public IAppEntity App { get; set; } + public T Entity { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/INotificationSender.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/INotificationSender.cs index 96de5b228..539c084ff 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/INotificationSender.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/INotificationSender.cs @@ -16,5 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Notifications Task SendUsageAsync(IUser user, string appName, long usage, long usageLimit); Task SendInviteAsync(IUser assigner, IUser user, string appName); + + Task SendTeamInviteAsync(IUser assigner, IUser user, string teamName); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopNotificationSender.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopNotificationSender.cs index b0da54368..6f9c3f71f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopNotificationSender.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopNotificationSender.cs @@ -21,6 +21,11 @@ namespace Squidex.Domain.Apps.Entities.Notifications return Task.CompletedTask; } + public Task SendTeamInviteAsync(IUser assigner, IUser user, string teamName) + { + return Task.CompletedTask; + } + public Task SendUsageAsync(IUser user, string appName, long usage, long limit) { return Task.CompletedTask; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs index 559ce216b..da2fc4295 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Notifications public IUser? Assigner { get; init; } - public string AppName { get; init; } + public string TeamName { get; init; } public long? ApiCalls { get; init; } @@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Notifications { ApiCalls = usage, ApiCallsLimit = usageLimit, - AppName = appName + TeamName = appName }; return SendEmailAsync("Usage", @@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Notifications Guard.NotNull(user); Guard.NotNull(appName); - var vars = new TemplatesVars { Assigner = assigner, AppName = appName }; + var vars = new TemplatesVars { Assigner = assigner, TeamName = appName }; if (user.Claims.HasConsent()) { @@ -97,6 +97,11 @@ namespace Squidex.Domain.Apps.Entities.Notifications } } + public Task SendTeamInviteAsync(IUser assigner, IUser user, string teamName) + { + return Task.CompletedTask; + } + private async Task SendEmailAsync(string template, string emailSubj, string emailBody, IUser user, TemplatesVars vars) { if (string.IsNullOrWhiteSpace(emailBody)) @@ -131,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.Notifications private static string Format(string text, TemplatesVars vars) { - text = text.Replace("$APP_NAME", vars.AppName, StringComparison.Ordinal); + text = text.Replace("$APP_NAME", vars.TeamName, StringComparison.Ordinal); if (vars.Assigner != null) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/_RuleCommand.cs similarity index 64% rename from backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/_RuleCommand.cs index f9fc9cec2..a65564715 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/_RuleCommand.cs @@ -8,17 +8,25 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +#pragma warning disable MA0048 // File name must match type name + namespace Squidex.Domain.Apps.Entities.Rules.Commands { - public abstract class RuleCommand : SquidexCommand, IAppCommand, IAggregateCommand + public abstract class RuleCommand : RuleCommandBase { - public NamedId AppId { get; set; } - public DomainId RuleId { get; set; } - public DomainId AggregateId + public override DomainId AggregateId { get => DomainId.Combine(AppId, RuleId); } } + + // This command is needed as marker for middlewares. + public abstract class RuleCommandBase : SquidexCommand, IAppCommand, IAggregateCommand + { + public NamedId AppId { get; set; } + + public abstract DomainId AggregateId { get; } + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs index 20a7db796..e7ed68744 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs @@ -45,7 +45,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject RuleDef = RuleDef.Rename(e.Name); AppId = e.AppId; - return true; } @@ -81,21 +80,18 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject case RuleEnabled: { RuleDef = RuleDef.Enable(); - break; } case RuleDisabled: { RuleDef = RuleDef.Disable(); - break; } case RuleDeleted: { IsDeleted = true; - return true; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs index a3ca8c9ef..87b390b25 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs @@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject protected override bool CanAcceptCreation(ICommand command) { - return command is RuleCommand; + return command is RuleCommandBase; } protected override bool CanAccept(ICommand command) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs index 692c4ea20..910f99b07 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Rules { - public sealed class RuleCommandMiddleware : AggregateCommandMiddleware + public sealed class RuleCommandMiddleware : AggregateCommandMiddleware { private readonly IRuleEnricher ruleEnricher; private readonly IContextProvider contextProvider; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs index b64695777..5f13caed3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class ChangeCategory : SchemaUpdateCommand + public sealed class ChangeCategory : SchemaCommand { public string? Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs index 2ba3bfa2a..b21fe6764 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Schemas; namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class ConfigureFieldRules : SchemaUpdateCommand + public sealed class ConfigureFieldRules : SchemaCommand { public FieldRuleCommand[]? FieldRules { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs index 710140670..3316dd897 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure.Collections; namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class ConfigurePreviewUrls : SchemaUpdateCommand + public sealed class ConfigurePreviewUrls : SchemaCommand { public ReadonlyDictionary? PreviewUrls { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs index 45182dcc5..0c7c1ad87 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Schemas; namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class ConfigureScripts : SchemaUpdateCommand + public sealed class ConfigureScripts : SchemaCommand { public SchemaScripts? Scripts { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureUIFields.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureUIFields.cs index 5d9c33211..9655cdab0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureUIFields.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureUIFields.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Schemas; namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class ConfigureUIFields : SchemaUpdateCommand + public sealed class ConfigureUIFields : SchemaCommand { public FieldNames? FieldsInLists { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs index 490f61fba..9856b9f5d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs @@ -9,11 +9,10 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Commands; -using SchemaField = Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaField; namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class CreateSchema : SchemaCommand, IUpsertCommand, IAggregateCommand + public sealed class CreateSchema : SchemaCommandBase, IUpsertCommand, IAggregateCommand { public DomainId SchemaId { get; set; } @@ -25,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands public SchemaType Type { get; set; } - public SchemaField[]? Fields { get; set; } + public UpsertSchemaField[]? Fields { get; set; } public FieldNames? FieldsInReferences { get; set; } @@ -51,9 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands public Schema BuildSchema() { - IUpsertCommand self = this; - - return self.ToSchema(Name, Type); + return ((IUpsertCommand)this).ToSchema(Name, Type); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs index 458779e5f..522dd9515 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class DeleteSchema : SchemaUpdateCommand + public sealed class DeleteSchema : SchemaCommand { } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs index 69e2425aa..91f067fb1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public abstract class ParentFieldCommand : SchemaUpdateCommand + public abstract class ParentFieldCommand : SchemaCommand { public long? ParentFieldId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs index 1395876d3..bde23a6bc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class PublishSchema : SchemaUpdateCommand + public sealed class PublishSchema : SchemaCommand { } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs index 260bb335b..182196876 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs @@ -12,7 +12,7 @@ using SchemaField = Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaFi namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class SynchronizeSchema : SchemaUpdateCommand, IUpsertCommand, IAggregateCommand, ISchemaCommand + public sealed class SynchronizeSchema : SchemaCommand, IUpsertCommand, IAggregateCommand, ISchemaCommand { public bool NoFieldDeletion { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs index d84289023..3652b963b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class UnpublishSchema : SchemaUpdateCommand + public sealed class UnpublishSchema : SchemaCommand { } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs index 82ca36219..37cf41263 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Schemas; namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public sealed class UpdateSchema : SchemaUpdateCommand + public sealed class UpdateSchema : SchemaCommand { public SchemaProperties Properties { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/_SchemaCommand.cs similarity index 59% rename from backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/_SchemaCommand.cs index d0cb2c6ed..247f6d5fb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/_SchemaCommand.cs @@ -6,10 +6,13 @@ // ========================================================================== using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +#pragma warning disable MA0048 // File name must match type name namespace Squidex.Domain.Apps.Entities.Schemas.Commands { - public abstract class SchemaUpdateCommand : SchemaCommand, ISchemaCommand + public abstract class SchemaCommand : SchemaCommandBase, ISchemaCommand { public NamedId SchemaId { get; set; } @@ -18,4 +21,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands get => DomainId.Combine(AppId, SchemaId.Id); } } + + // This command is needed as marker for middlewares. + public abstract class SchemaCommandBase : SquidexCommand, IAppCommand, IAggregateCommand + { + public NamedId AppId { get; set; } + + public abstract DomainId AggregateId { get; } + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs index f2a7f1e79..f482a6b5a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs @@ -48,7 +48,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject SchemaFieldsTotal = e.Schema.MaxId(); AppId = e.AppId; - return true; } @@ -71,7 +70,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject } SchemaFieldsTotal = Math.Max(SchemaFieldsTotal, e.FieldId.Id); - break; } @@ -93,112 +91,96 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject case SchemaCategoryChanged e: { SchemaDef = SchemaDef.ChangeCategory(e.Name); - break; } case SchemaPreviewUrlsConfigured e: { SchemaDef = SchemaDef.SetPreviewUrls(e.PreviewUrls); - break; } case SchemaScriptsConfigured e: { SchemaDef = SchemaDef.SetScripts(e.Scripts); - break; } case SchemaFieldRulesConfigured e: { SchemaDef = SchemaDef.SetFieldRules(e.FieldRules); - break; } case SchemaPublished: { SchemaDef = SchemaDef.Publish(); - break; } case SchemaUnpublished: { SchemaDef = SchemaDef.Unpublish(); - break; } case SchemaUpdated e: { SchemaDef = SchemaDef.Update(e.Properties); - break; } case SchemaFieldsReordered e: { SchemaDef = SchemaDef.ReorderFields(e.FieldIds.ToList(), e.ParentFieldId?.Id); - break; } case FieldUpdated e: { SchemaDef = SchemaDef.UpdateField(e.FieldId.Id, e.Properties, e.ParentFieldId?.Id); - break; } case FieldLocked e: { SchemaDef = SchemaDef.LockField(e.FieldId.Id, e.ParentFieldId?.Id); - break; } case FieldDisabled e: { SchemaDef = SchemaDef.DisableField(e.FieldId.Id, e.ParentFieldId?.Id); - break; } case FieldEnabled e: { SchemaDef = SchemaDef.EnableField(e.FieldId.Id, e.ParentFieldId?.Id); - break; } case FieldHidden e: { SchemaDef = SchemaDef.HideField(e.FieldId.Id, e.ParentFieldId?.Id); - break; } case FieldShown e: { SchemaDef = SchemaDef.ShowField(e.FieldId.Id, e.ParentFieldId?.Id); - break; } case FieldDeleted e: { SchemaDef = SchemaDef.DeleteField(e.FieldId.Id, e.ParentFieldId?.Id); - break; } case SchemaDeleted: { IsDeleted = true; - return true; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs index 06b47d7e1..ff8841464 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs @@ -36,12 +36,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject protected override bool CanAcceptCreation(ICommand command) { - return command is SchemaCommand; + return command is SchemaCommandBase; } protected override bool CanAccept(ICommand command) { - return command is SchemaUpdateCommand schemaCommand && + return command is SchemaCommand schemaCommand && Equals(schemaCommand.AppId, Snapshot.AppId) && Equals(schemaCommand.SchemaId?.Id, Snapshot.Id); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs index e4777b161..320f8aaea 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs @@ -144,7 +144,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes case DeleteSchema delete: await OnDeleteAsync(delete); break; - case SchemaUpdateCommand update: + case SchemaCommand update: await OnUpdateAsync(update); break; } @@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes return InvalidateItAsync(delete.AppId.Id, delete.SchemaId.Id, delete.SchemaId.Name); } - private Task OnUpdateAsync(SchemaUpdateCommand update) + private Task OnUpdateAsync(SchemaCommand update) { return InvalidateItAsync(update.AppId.Id, update.SchemaId.Id, update.SchemaId.Name); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/AssignContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/AssignContributor.cs new file mode 100644 index 000000000..bab461dd4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/AssignContributor.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Roles = Squidex.Domain.Apps.Core.Apps.Role; + +namespace Squidex.Domain.Apps.Entities.Teams.Commands +{ + public sealed class AssignContributor : TeamCommand + { + public string ContributorId { get; set; } + + public string Role { get; set; } = Roles.Owner; + + public bool IgnoreActor { get; set; } + + public bool IgnorePlans { get; set; } + + public bool Invite { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/ChangePlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/ChangePlan.cs new file mode 100644 index 000000000..6a738d416 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/ChangePlan.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Teams.Commands +{ + public sealed class ChangePlan : TeamCommand + { + public bool FromCallback { get; set; } + + public string PlanId { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/CreateTeam.cs similarity index 63% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/CreateTeam.cs index d384884bf..6525ff913 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/CreateTeam.cs @@ -7,15 +7,15 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Teams.Commands { - public abstract class AppUpdateCommand : AppCommand, IAppCommand + public sealed class CreateTeam : TeamCommand { - public NamedId AppId { get; set; } + public string Name { get; set; } - public override DomainId AggregateId + public CreateTeam() { - get => AppId.Id; + TeamId = DomainId.NewGuid(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/RemoveContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/RemoveContributor.cs new file mode 100644 index 000000000..ac0b57a17 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/RemoveContributor.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Teams.Commands +{ + public sealed class RemoveContributor : TeamCommand + { + public string ContributorId { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/UpdateTeam.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/UpdateTeam.cs new file mode 100644 index 000000000..4ec1383fe --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/UpdateTeam.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Teams.Commands +{ + public sealed class UpdateTeam : TeamCommand + { + public string Name { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/_TeamCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/_TeamCommand.cs new file mode 100644 index 000000000..8a08e530a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/_TeamCommand.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +#pragma warning disable MA0048 // File name must match type name + +namespace Squidex.Domain.Apps.Entities.Teams.Commands +{ + public abstract class TeamCommand : TeamCommandBase, ITeamCommand + { + public DomainId TeamId { get; set; } + + public override DomainId AggregateId + { + get => TeamId; + } + } + + // This command is needed as marker for middlewares. + public abstract class TeamCommandBase : SquidexCommand, IAggregateCommand + { + public abstract DomainId AggregateId { get; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeam.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeam.cs new file mode 100644 index 000000000..e61ed1978 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeam.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Billing; +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Translations; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject.Guards +{ + public static class GuardTeam + { + public static void CanCreate(CreateTeam command) + { + Guard.NotNull(command); + + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined(nameof(command.Name)), nameof(command.Name)); + } + }); + } + + public static void CanUpdate(UpdateTeam command) + { + Guard.NotNull(command); + + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined(nameof(command.Name)), nameof(command.Name)); + } + }); + } + + public static void CanChangePlan(ChangePlan command, IBillingPlans billingPlans) + { + Guard.NotNull(command); + + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.PlanId)) + { + e(Not.Defined(nameof(command.PlanId)), nameof(command.PlanId)); + return; + } + + if (billingPlans.GetPlan(command.PlanId) == null) + { + e(T.Get("apps.plans.notFound"), nameof(command.PlanId)); + } + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeamContributors.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeamContributors.cs new file mode 100644 index 000000000..24b9d45a2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeamContributors.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Translations; +using Squidex.Infrastructure.Validation; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject.Guards +{ + public static class GuardTeamContributors + { + public static Task CanAssign(AssignContributor command, ITeamEntity team, IUserResolver users) + { + Guard.NotNull(command); + + var contributors = team.Contributors; + + return Validate.It(async e => + { + if (command.Role != Role.Owner) + { + e(Not.Valid(nameof(command.Role)), nameof(command.Role)); + } + + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + e(Not.Defined(nameof(command.ContributorId)), nameof(command.ContributorId)); + } + else + { + var user = await users.FindByIdAsync(command.ContributorId); + + if (user == null) + { + throw new DomainObjectNotFoundException(command.ContributorId); + } + + if (!command.IgnoreActor && string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase)) + { + throw new DomainForbiddenException(T.Get("apps.contributors.cannotChangeYourself")); + } + } + }); + } + + public static void CanRemove(RemoveContributor command, ITeamEntity team) + { + Guard.NotNull(command); + + var contributors = team.Contributors; + + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + e(Not.Defined(nameof(command.ContributorId)), nameof(command.ContributorId)); + } + + var ownerIds = contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToList(); + + if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) + { + e(T.Get("apps.contributors.onlyOneOwner")); + } + }); + + if (!contributors.ContainsKey(command.ContributorId)) + { + throw new DomainObjectNotFoundException(command.ContributorId); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.State.cs new file mode 100644 index 000000000..7d536bbd6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.State.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json.Serialization; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Events.Teams; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject +{ + public partial class TeamDomainObject + { + public sealed class State : DomainObjectState, ITeamEntity + { + public string Name { get; set; } + + public Contributors Contributors { get; set; } = Contributors.Empty; + + public AssignedPlan? Plan { get; set; } + + [JsonIgnore] + public DomainId UniqueId + { + get => Id; + } + + public override bool ApplyEvent(IEvent @event) + { + switch (@event) + { + case TeamCreated e: + { + Id = e.TeamId; + + SimpleMapper.Map(e, this); + return true; + } + + case TeamUpdated e when Is.Change(Name, e.Name): + { + SimpleMapper.Map(e, this); + return true; + } + + case TeamPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId): + return UpdatePlan(e.ToPlan()); + + case TeamPlanReset e when Plan != null: + return UpdatePlan(null); + + case TeamContributorAssigned e: + return UpdateContributors(e, (e, c) => c.Assign(e.ContributorId, e.Role)); + + case TeamContributorRemoved e: + return UpdateContributors(e, (e, c) => c.Remove(e.ContributorId)); + } + + return false; + } + + private bool UpdateContributors(T @event, Func update) + { + var previous = Contributors; + + Contributors = update(@event, previous); + + return !ReferenceEquals(previous, Contributors); + } + + private bool UpdatePlan(AssignedPlan? plan) + { + Plan = plan; + + return true; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.cs new file mode 100644 index 000000000..1b5d28b1c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.cs @@ -0,0 +1,225 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Billing; +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Domain.Apps.Entities.Teams.DomainObject.Guards; +using Squidex.Domain.Apps.Events.Teams; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; +using Squidex.Shared.Users; + +#pragma warning disable MA0022 // Return Task.FromResult instead of returning null + +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject +{ + public partial class TeamDomainObject : DomainObject + { + private readonly IServiceProvider serviceProvider; + + public TeamDomainObject(DomainId id, IPersistenceFactory persistence, ILogger log, + IServiceProvider serviceProvider) + : base(id, persistence, log) + { + this.serviceProvider = serviceProvider; + } + + protected override bool IsDeleted(State snapshot) + { + return false; + } + + protected override bool CanAcceptCreation(ICommand command) + { + return command is TeamCommandBase; + } + + protected override bool CanAccept(ICommand command) + { + return command is TeamCommand update && Equals(update?.TeamId, Snapshot.Id); + } + + public override Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct) + { + switch (command) + { + case CreateTeam create: + return CreateReturn(create, c => + { + GuardTeam.CanCreate(c); + + Create(c); + + return Snapshot; + }, ct); + + case UpdateTeam update: + return UpdateReturn(update, c => + { + GuardTeam.CanUpdate(c); + + Update(c); + + return Snapshot; + }, ct); + + case AssignContributor assignContributor: + return UpdateReturnAsync(assignContributor, async (c, ct) => + { + await GuardTeamContributors.CanAssign(c, Snapshot, Users()); + + AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId)); + + return Snapshot; + }, ct); + + case RemoveContributor removeContributor: + return UpdateReturn(removeContributor, c => + { + GuardTeamContributors.CanRemove(c, Snapshot); + + RemoveContributor(c); + + return Snapshot; + }, ct); + + case ChangePlan changePlan: + return ChangeBillingPlanAsync(changePlan, ct); + + default: + ThrowHelper.NotSupportedException(); + return default!; + } + } + + private async Task ChangeBillingPlanAsync(ChangePlan changePlan, + CancellationToken ct) + { + var userId = changePlan.Actor.Identifier; + + var result = await UpdateReturnAsync(changePlan, async (c, ct) => + { + GuardTeam.CanChangePlan(c, BillingPlans()); + + if (string.Equals(GetFreePlan()?.Id, c.PlanId, StringComparison.Ordinal)) + { + ResetPlan(c); + + return new PlanChangedResult(c.PlanId, true, null); + } + + if (!c.FromCallback) + { + var redirectUri = await BillingManager().MustRedirectToPortalAsync(userId, UniqueId, c.PlanId, ct); + + if (redirectUri != null) + { + return new PlanChangedResult(c.PlanId, false, redirectUri); + } + } + + ChangePlan(c); + + return new PlanChangedResult(c.PlanId); + }, ct); + + if (changePlan.FromCallback) + { + return result; + } + + if (result.Payload is PlanChangedResult { Unsubscribed: true, RedirectUri: null }) + { + await BillingManager().UnsubscribeAsync(userId, UniqueId, default); + } + else if (result.Payload is PlanChangedResult { RedirectUri: null }) + { + await BillingManager().SubscribeAsync(userId, UniqueId, changePlan.PlanId, default); + } + + return result; + } + + private void Create(CreateTeam command) + { + void RaiseInitial(T @event) where T : TeamEvent + { + Raise(command, @event, command.TeamId); + } + + RaiseInitial(new TeamCreated()); + + var actor = command.Actor; + + if (actor.IsUser) + { + RaiseInitial(new TeamContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }); + } + } + + private void ChangePlan(ChangePlan command) + { + Raise(command, new TeamPlanChanged()); + } + + private void ResetPlan(ChangePlan command) + { + Raise(command, new TeamPlanReset()); + } + + private void Update(UpdateTeam command) + { + Raise(command, new TeamUpdated()); + } + + private void AssignContributor(AssignContributor command, bool isAdded) + { + Raise(command, new TeamContributorAssigned { IsAdded = isAdded }); + } + + private void RemoveContributor(RemoveContributor command) + { + Raise(command, new TeamContributorRemoved()); + } + + private void Raise(T command, TEvent @event, DomainId? id = null) where T : class where TEvent : TeamEvent + { + SimpleMapper.Map(command, @event); + + @event.TeamId = id ?? Snapshot.Id; + + RaiseEvent(Envelope.Create(@event)); + } + + private IBillingPlans BillingPlans() + { + return serviceProvider.GetRequiredService(); + } + + private IBillingManager BillingManager() + { + return serviceProvider.GetRequiredService(); + } + + private IUserResolver Users() + { + return serviceProvider.GetRequiredService(); + } + + private Plan GetFreePlan() + { + return BillingPlans().GetFreePlan(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/ITeamEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/ITeamEntity.cs new file mode 100644 index 000000000..891391852 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/ITeamEntity.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core; + +namespace Squidex.Domain.Apps.Entities.Teams +{ + public interface ITeamEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion + { + string Name { get; } + + Contributors Contributors { get; } + + AssignedPlan? Plan { get; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/ITeamsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/ITeamsIndex.cs new file mode 100644 index 000000000..b8ea6f19f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/ITeamsIndex.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Teams.Indexes +{ + public interface ITeamsIndex + { + Task GetTeamAsync(DomainId id, + CancellationToken ct = default); + + Task> GetTeamsAsync(string userId, + CancellationToken ct = default); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/TeamsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/TeamsIndex.cs new file mode 100644 index 000000000..c64393b8e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/TeamsIndex.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Teams.Repositories; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Teams.Indexes +{ + public sealed class TeamsIndex : ITeamsIndex + { + private readonly ITeamRepository teamRepository; + + public TeamsIndex(ITeamRepository teamRepository) + { + this.teamRepository = teamRepository; + } + + public async Task GetTeamAsync(DomainId id, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("TeamsIndex/GetTeamAsync")) + { + var team = await teamRepository.FindAsync(id, ct); + + return IsValid(team) ? team : null; + } + } + + public async Task> GetTeamsAsync(string userId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("TeamsIndex/GetTeamsAsync")) + { + var teams = await teamRepository.QueryAllAsync(userId, ct); + + return teams.Where(IsValid).ToList(); + } + } + + private static bool IsValid(ITeamEntity? rule) + { + return rule is { Version: > EtagVersion.Empty }; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Repositories/ITeamRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Repositories/ITeamRepository.cs new file mode 100644 index 000000000..afe19510e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Repositories/ITeamRepository.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Teams.Repositories +{ + public interface ITeamRepository + { + Task> QueryAllAsync(string contributorId, + CancellationToken ct = default); + + Task FindAsync(DomainId id, + CancellationToken ct = default); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamExtensions.cs new file mode 100644 index 000000000..d424621dc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamExtensions.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Diagnostics.CodeAnalysis; + +namespace Squidex.Domain.Apps.Entities.Teams +{ + public static class TeamExtensions + { + public static bool TryGetContributorRole(this ITeamEntity app, string id, [MaybeNullWhen(false)] out string role) + { + return app.Contributors.TryGetValue(id, out role); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs new file mode 100644 index 000000000..760e759e5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Events.Teams; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Teams.Entities.Teams +{ + public sealed class TeamHistoryEventsCreator : HistoryEventsCreatorBase + { + public TeamHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) + { + AddEventMessage( + "history.teams.contributoreAssigned"); + + AddEventMessage( + "history.teams.contributoreRemoved"); + + AddEventMessage( + "history.teams.planChanged"); + + AddEventMessage( + "history.teams.planReset"); + + AddEventMessage( + "history.teams.updated"); + } + + private HistoryEvent? CreateEvent(IEvent @event) + { + switch (@event) + { + case TeamContributorAssigned e: + return CreateContributorsEvent(e, e.ContributorId, e.Role); + case TeamContributorRemoved e: + return CreateContributorsEvent(e, e.ContributorId); + case TeamPlanChanged e: + return CreatePlansEvent(e, e.PlanId); + case TeamPlanReset e: + return CreatePlansEvent(e); + case TeamUpdated e: + return CreateGeneralEvent(e); + } + + return null; + } + + private HistoryEvent CreateGeneralEvent(IEvent e) + { + return ForEvent(e, "settings.general"); + } + + private HistoryEvent CreateContributorsEvent(IEvent e, string contributor, string? role = null) + { + return ForEvent(e, "settings.contributors").Param("Contributor", contributor).Param("Role", role); + } + + private HistoryEvent CreatePlansEvent(IEvent e, string? plan = null) + { + return ForEvent(e, "settings.plan").Param("Plan", plan); + } + + protected override Task CreateEventCoreAsync(Envelope @event) + { + return Task.FromResult(CreateEvent(@event.Payload)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs index 9745430a1..946c9ffcc 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Apps @@ -15,9 +15,9 @@ namespace Squidex.Domain.Apps.Events.Apps { public string PlanId { get; set; } - public AppPlan ToPlan() + public AssignedPlan ToPlan() { - return new AppPlan(Actor, PlanId); + return new AssignedPlan(Actor, PlanId); } } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppTransfered.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppTransfered.cs new file mode 100644 index 000000000..f00e74a0b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppTransfered.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Apps +{ + [EventType(nameof(AppTransfered))] + public sealed class AppTransfered : AppEvent + { + public DomainId? TeamId { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorAssigned.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorAssigned.cs new file mode 100644 index 000000000..a1b9f510a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorAssigned.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Teams +{ + [EventType(nameof(TeamContributorAssigned))] + public sealed class TeamContributorAssigned : TeamEvent + { + public string ContributorId { get; set; } + + public string Role { get; set; } + + public bool IsCreated { get; set; } + + public bool IsAdded { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorRemoved.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorRemoved.cs new file mode 100644 index 000000000..6ebcf4264 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorRemoved.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Teams +{ + [EventType(nameof(TeamContributorRemoved))] + public sealed class TeamContributorRemoved : TeamEvent + { + public string ContributorId { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamCreated.cs new file mode 100644 index 000000000..5c87b4dc0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamCreated.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Teams +{ + [EventType(nameof(TeamCreated))] + public sealed class TeamCreated : TeamEvent + { + public string Name { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamEvent.cs new file mode 100644 index 000000000..271600d81 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamEvent.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Events.Teams +{ + public abstract class TeamEvent : SquidexEvent + { + public DomainId TeamId { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanChanged.cs new file mode 100644 index 000000000..7db4d6553 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanChanged.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Teams +{ + [EventType(nameof(TeamPlanChanged))] + public sealed class TeamPlanChanged : TeamEvent + { + public string PlanId { get; set; } + + public AssignedPlan ToPlan() + { + return new AssignedPlan(Actor, PlanId); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanReset.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanReset.cs new file mode 100644 index 000000000..ba44673c9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanReset.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Teams +{ + [EventType(nameof(TeamPlanReset))] + public sealed class TeamPlanReset : TeamEvent + { + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamUpdated.cs new file mode 100644 index 000000000..ddb8c3c7b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamUpdated.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Teams +{ + [EventType(nameof(TeamUpdated))] + public sealed class TeamUpdated : TeamEvent + { + public string Name { get; set; } + } +} diff --git a/backend/src/Squidex.Shared/PermissionExtensions.cs b/backend/src/Squidex.Shared/PermissionExtensions.cs index b82bf48c3..e7dd49925 100644 --- a/backend/src/Squidex.Shared/PermissionExtensions.cs +++ b/backend/src/Squidex.Shared/PermissionExtensions.cs @@ -12,9 +12,9 @@ namespace Squidex.Shared { public static class PermissionExtensions { - public static bool Allows(this PermissionSet permissions, string id, string app = Permission.Any, string schema = Permission.Any) + public static bool Allows(this PermissionSet permissions, string id, string app = Permission.Any, string schema = Permission.Any, string team = Permission.Any) { - var permission = PermissionIds.ForApp(id, app, schema); + var permission = PermissionIds.ForApp(id, app, schema, team); return permissions.Allows(permission); } diff --git a/backend/src/Squidex.Shared/PermissionIds.cs b/backend/src/Squidex.Shared/PermissionIds.cs index 35336494d..64d3e4e96 100644 --- a/backend/src/Squidex.Shared/PermissionIds.cs +++ b/backend/src/Squidex.Shared/PermissionIds.cs @@ -20,6 +20,9 @@ namespace Squidex.Shared // Admin App Creation public const string AdminAppCreate = "squidex.admin.apps.create"; + // Admin Team Creation + public const string AdminTeamCreate = "squidex.admin.teams.create"; + // Backup Admin public const string AdminRestore = "squidex.admin.restore"; @@ -36,11 +39,37 @@ namespace Squidex.Shared public const string AdminUsersUnlock = "squidex.admin.users.unlock"; public const string AdminUsersLock = "squidex.admin.users.lock"; + // Team + public const string Team = "squidex.teams.{team}"; + + // Team General + public const string TeamAdmin = "squidex.teams.{team}.*"; + public const string TeamUpdate = "squidex.teams.{team}.update"; + + // Team Contributors + public const string TeamContributors = "squidex.teams.{team}.contributors"; + public const string TeamContributorsRead = "squidex.teams.{team}.contributors.read"; + public const string TeamContributorsAssign = "squidex.teams.{team}.contributors.assign"; + public const string TeamContributorsRevoke = "squidex.teams.{team}.contributors.revoke"; + + // Team Plans + public const string TeamPlans = "squidex.teams.{team}.plans"; + public const string TeamPlansRead = "squidex.teams.{team}.plans.read"; + public const string TeamPlansChange = "squidex.teams.{team}.plans.change"; + + // Team Usage + public const string TeamUsage = "squidex.teams.{team}.usage"; + + // Team History + public const string TeamHistory = "squidex.teams.{team}.history"; + + // App public const string App = "squidex.apps.{app}"; // App General public const string AppAdmin = "squidex.apps.{app}.*"; public const string AppDelete = "squidex.apps.{app}.delete"; + public const string AppTransfer = "squidex.apps.{app}.transfer"; public const string AppUpdate = "squidex.apps.{app}.update"; public const string AppUpdateSettings = "squidex.apps.{app}.settings"; @@ -48,75 +77,75 @@ namespace Squidex.Shared public const string AppImageUpload = "squidex.apps.{app}.image"; public const string AppImageDelete = "squidex.apps.{app}.image"; - // History + // App History public const string AppHistory = "squidex.apps.{app}.history"; - // Ping + // App Ping public const string AppPing = "squidex.apps.{app}.ping"; - // Search + // App Search public const string AppSearch = "squidex.apps.{app}.search"; - // Translate + // App Translate public const string AppTranslate = "squidex.apps.{app}.translate"; - // Usage + // App Usage public const string AppUsage = "squidex.apps.{app}.usage"; - // Comments + // App Comments public const string AppComments = "squidex.apps.{app}.comments"; public const string AppCommentsRead = "squidex.apps.{app}.comments.read"; public const string AppCommentsCreate = "squidex.apps.{app}.comments.create"; public const string AppCommentsUpdate = "squidex.apps.{app}.comments.update"; public const string AppCommentsDelete = "squidex.apps.{app}.comments.delete"; - // Clients + // App Clients public const string AppClients = "squidex.apps.{app}.clients"; public const string AppClientsRead = "squidex.apps.{app}.clients.read"; public const string AppClientsCreate = "squidex.apps.{app}.clients.create"; public const string AppClientsUpdate = "squidex.apps.{app}.clients.update"; public const string AppClientsDelete = "squidex.apps.{app}.clients.delete"; - // Contributors + // App Contributors public const string AppContributors = "squidex.apps.{app}.contributors"; public const string AppContributorsRead = "squidex.apps.{app}.contributors.read"; public const string AppContributorsAssign = "squidex.apps.{app}.contributors.assign"; public const string AppContributorsRevoke = "squidex.apps.{app}.contributors.revoke"; - // Languages + // App Languages public const string AppLanguages = "squidex.apps.{app}.languages"; public const string AppLanguagesRead = "squidex.apps.{app}.languages.read"; public const string AppLanguagesCreate = "squidex.apps.{app}.languages.create"; public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update"; public const string AppLanguagesDelete = "squidex.apps.{app}.languages.delete"; - // Roles + // App Roles public const string AppRoles = "squidex.apps.{app}.roles"; public const string AppRolesRead = "squidex.apps.{app}.roles.read"; public const string AppRolesCreate = "squidex.apps.{app}.roles.create"; public const string AppRolesUpdate = "squidex.apps.{app}.roles.update"; public const string AppRolesDelete = "squidex.apps.{app}.roles.delete"; - // Workflows + // App Workflows public const string AppWorkflows = "squidex.apps.{app}.workflows"; public const string AppWorkflowsRead = "squidex.apps.{app}.workflows.read"; public const string AppWorkflowsCreate = "squidex.apps.{app}.workflows.create"; public const string AppWorkflowsUpdate = "squidex.apps.{app}.workflows.update"; public const string AppWorkflowsDelete = "squidex.apps.{app}.workflows.delete"; - // Backups + // App Backups public const string AppBackups = "squidex.apps.{app}.backups"; public const string AppBackupsRead = "squidex.apps.{app}.backups.read"; public const string AppBackupsCreate = "squidex.apps.{app}.backups.create"; public const string AppBackupsDelete = "squidex.apps.{app}.backups.delete"; public const string AppBackupsDownload = "squidex.apps.{app}.backups.download"; - // Plans + // App Plans public const string AppPlans = "squidex.apps.{app}.plans"; public const string AppPlansRead = "squidex.apps.{app}.plans.read"; public const string AppPlansChange = "squidex.apps.{app}.plans.change"; - // Assets + // App Assets public const string AppAssets = "squidex.apps.{app}.assets"; public const string AppAssetsRead = "squidex.apps.{app}.assets.read"; public const string AppAssetsCreate = "squidex.apps.{app}.assets.create"; @@ -124,24 +153,27 @@ namespace Squidex.Shared public const string AppAssetsUpdate = "squidex.apps.{app}.assets.update"; public const string AppAssetsDelete = "squidex.apps.{app}.assets.delete"; + // App Asset Scripts public const string AppAssetScripts = "squidex.apps.{app}.asset-scripts"; public const string AppAssetSScriptsRead = "squidex.apps.{app}.asset-scripts.read"; public const string AppAssetsScriptsUpdate = "squidex.apps.{app}.asset-scripts.update"; - // Rules + // App Rules public const string AppRules = "squidex.apps.{app}.rules"; public const string AppRulesRead = "squidex.apps.{app}.rules.read"; + public const string AppRulesCreate = "squidex.apps.{app}.rules.create"; + public const string AppRulesUpdate = "squidex.apps.{app}.rules.update"; + public const string AppRulesDisable = "squidex.apps.{app}.rules.disable"; + public const string AppRulesDelete = "squidex.apps.{app}.rules.delete"; + + // App Rule Events public const string AppRulesEvents = "squidex.apps.{app}.rules.events"; public const string AppRulesEventsRun = "squidex.apps.{app}.rules.events.run"; public const string AppRulesEventsRead = "squidex.apps.{app}.rules.events.read"; public const string AppRulesEventsUpdate = "squidex.apps.{app}.rules.events.update"; public const string AppRulesEventsDelete = "squidex.apps.{app}.rules.events.delete"; - public const string AppRulesCreate = "squidex.apps.{app}.rules.create"; - public const string AppRulesUpdate = "squidex.apps.{app}.rules.update"; - public const string AppRulesDisable = "squidex.apps.{app}.rules.disable"; - public const string AppRulesDelete = "squidex.apps.{app}.rules.delete"; - // Schemas + // App Schemas public const string AppSchemas = "squidex.apps.{app}.schemas"; public const string AppSchemasRead = "squidex.apps.{app}.schemas.read"; public const string AppSchemasCreate = "squidex.apps.{app}.schemas.create"; @@ -150,7 +182,7 @@ namespace Squidex.Shared public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{schema}.publish"; public const string AppSchemasDelete = "squidex.apps.{app}.schemas.{schema}.delete"; - // Contents + // App Contents public const string AppContents = "squidex.apps.{app}.contents.{schema}"; public const string AppContentsRead = "squidex.apps.{app}.contents.{schema}.read"; public const string AppContentsReadOwn = "squidex.apps.{app}.contents.{schema}.read.own"; @@ -169,12 +201,13 @@ namespace Squidex.Shared public const string AppContentsDelete = "squidex.apps.{app}.contents.{schema}.delete"; public const string AppContentsDeleteOwn = "squidex.apps.{app}.contents.{schema}.delete.own"; - public static Permission ForApp(string id, string app = Permission.Any, string schema = Permission.Any) + public static Permission ForApp(string id, string app = Permission.Any, string schema = Permission.Any, string team = Permission.Any) { Guard.NotNull(id); id = id.Replace("{app}", app ?? Permission.Any, StringComparison.Ordinal); id = id.Replace("{schema}", schema ?? Permission.Any, StringComparison.Ordinal); + id = id.Replace("{team}", team ?? Permission.Any, StringComparison.Ordinal); return new Permission(id); } diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index 1b093d2c2..483f3acd1 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -142,6 +142,9 @@ Il file non è una immagine + + Plan is managed by the team. + Non esiste un piano con questo id. @@ -163,6 +166,12 @@ Non è possibile rimuovere un ruolo quando questo è assegnato ad un collaboratore. + + Subscription must be cancelled first before the app can be transfered. + + + The team does not exist. + La cartella delle risorse non esiste. @@ -709,6 +718,9 @@ L'entità ({id}) richiede la versione {expectedVersion}, ma è stata trovata {currentVersion}. + + updated asset scripts + aggiunto client {[Id]} all'app @@ -724,6 +736,12 @@ Rimosso {user:[Contributor]} dall'app + + removed app image + + + uploaded a new app image + aggiunta lingua {[Language]} @@ -754,6 +772,12 @@ updated UI settings + + updated app to client + + + updated general settings + ha sostituito la risorsa. @@ -829,6 +853,21 @@ ha cambiato lo stato del contenuto {[Schema]} in {[Status]}. + + assigned {user:[Contributor]} as {[Role]} + + + removed {user:[Contributor]} from team + + + changed plan to {[Plan]} + + + resetted plan + + + updated general settings + Il tuo indirizzo email è impostato su privato in Github. Impostalo come pubblico per poter utilizzare il login Github. @@ -964,6 +1003,12 @@ Checklist di Sistema + + With your setup, only admins can create new teams. If you want to change this set <code>UI__ONLYADMINSCANCREATETEAMS=false</code> as environment variable. + + + With your setup, every user can create new teams. If you want to change this set <code>UI__ONLYADMINSCANCREATETEAMS=true</code> as environment variable. + Dovresti accedere a Squidex solo con un URL canonico e configurare questo URL sulla variabile d'ambiente <code> URLS__BASEURL </code>. Il base URL corrente <code> {actual} </code> non corrisponde al base URL <code>{configured}</code>. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index d6f0d07a9..9db1e3e7d 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -142,6 +142,9 @@ Bestand is geen afbeelding + + Plan is managed by the team. + Een plan met deze id bestaat niet. @@ -163,6 +166,12 @@ Kan een rol niet verwijderen wanneer een bijdrager is toegewezen. + + Subscription must be cancelled first before the app can be transfered. + + + The team does not exist. + Assetmap bestaat niet. @@ -709,6 +718,9 @@ Entiteit ({id}) heeft versie {verwachteVersion} aangevraagd, maar heeft {currentVersion} gevonden. + + updated asset scripts + client {[Id]} toegevoegd aan app @@ -724,6 +736,12 @@ heeft {user:[Contributor]} verwijderd uit app + + removed app image + + + uploaded a new app image + taal toegevoegd {[Language]} @@ -754,6 +772,12 @@ Bijgewerkte UI instellingen + + updated app to client + + + updated general settings + item vervangen. @@ -829,6 +853,21 @@ veranderde status van {[Schema]} inhoud in {[Status]}. + + assigned {user:[Contributor]} as {[Role]} + + + removed {user:[Contributor]} from team + + + changed plan to {[Plan]} + + + resetted plan + + + updated general settings + Jouw e-mailadres is ingesteld op privé in Github. Stel het in op openbaar om Github-login te gebruiken. @@ -964,6 +1003,12 @@ System Checklist + + With your setup, only admins can create new teams. If you want to change this set <code>UI__ONLYADMINSCANCREATETEAMS=false</code> as environment variable. + + + With your setup, every user can create new teams. If you want to change this set <code>UI__ONLYADMINSCANCREATETEAMS=true</code> as environment variable. + You should access Squidex only over one canonical URL and configure this URL with the <code>URLS__BASEURL</code> environment variable. The current base URL <code>{actual}</code> does not match to the base url <code>{configured}</code>. This variable must point to the public URL under which your Squidex instance is available. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index 5dcfdb078..d171ce3d7 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -142,6 +142,9 @@ File is not an image + + Plan is managed by the team. + A plan with this id does not exist. @@ -163,6 +166,12 @@ Cannot remove a role when a contributor is assigned. + + Subscription must be cancelled first before the app can be transfered. + + + The team does not exist. + Asset folder does not exist. @@ -709,6 +718,9 @@ Entity ({id}) requested version {expectedVersion}, but found {currentVersion}. + + updated asset scripts + added client {[Id]} to app @@ -724,6 +736,12 @@ removed {user:[Contributor]} from app + + removed app image + + + uploaded a new app image + added language {[Language]} @@ -754,6 +772,12 @@ updated UI settings + + updated app to client + + + updated general settings + replaced asset. @@ -829,6 +853,21 @@ changed status of {[Schema]} content to {[Status]}. + + assigned {user:[Contributor]} as {[Role]} + + + removed {user:[Contributor]} from team + + + changed plan to {[Plan]} + + + resetted plan + + + updated general settings + Your email address is set to private in Github. Please set it to public to use Github login. @@ -964,6 +1003,12 @@ System Checklist + + With your setup, only admins can create new teams. If you want to change this set <code>UI__ONLYADMINSCANCREATETEAMS=false</code> as environment variable. + + + With your setup, every user can create new teams. If you want to change this set <code>UI__ONLYADMINSCANCREATETEAMS=true</code> as environment variable. + You should access Squidex only over one canonical URL and configure this URL with the <code>URLS__BASEURL</code> environment variable. The current base URL <code>{actual}</code> does not match to the base url <code>{configured}</code>. This variable must point to the public URL under which your Squidex instance is available. diff --git a/backend/src/Squidex.Shared/Texts.zh.resx b/backend/src/Squidex.Shared/Texts.zh.resx index 2246193e5..0a17914ef 100644 --- a/backend/src/Squidex.Shared/Texts.zh.resx +++ b/backend/src/Squidex.Shared/Texts.zh.resx @@ -142,6 +142,9 @@ 文件不是图像 + + Plan is managed by the team. + 不存在具有此 ID 的计划。 @@ -163,6 +166,12 @@ 当分配了贡献者时无法删除角色。 + + Subscription must be cancelled first before the app can be transfered. + + + The team does not exist. + 资源文件夹不存在。 @@ -709,6 +718,9 @@ 实体 ({id}) 请求版本 {expectedVersion},但找到 {currentVersion}。 + + updated asset scripts + 将客户端 {[Id]} 添加到应用程序 @@ -724,6 +736,12 @@ 从应用中删除了 {user:[Contributor]} + + removed app image + + + uploaded a new app image + 添加语言 {[Language]} @@ -754,6 +772,12 @@ 更新的 UI 设置 + + updated app to client + + + updated general settings + 替换的资源。 @@ -829,6 +853,21 @@ 已将 {[Schema]} 内容的状态更改为 {[Status]}。 + + assigned {user:[Contributor]} as {[Role]} + + + removed {user:[Contributor]} from team + + + changed plan to {[Plan]} + + + resetted plan + + + updated general settings + 您的邮箱在 Github 中设置为私有。请设置为公开以使用 Github 登录。 @@ -964,6 +1003,12 @@ 系统清单 + + With your setup, only admins can create new teams. If you want to change this set <code>UI__ONLYADMINSCANCREATETEAMS=false</code> as environment variable. + + + With your setup, every user can create new teams. If you want to change this set <code>UI__ONLYADMINSCANCREATETEAMS=true</code> as environment variable. + 您应该仅通过一个规范 URL 访问 Squidex,并通过 <code>URLS__BASEURL</code> 环境变量配置此 URL。当前的基本 URL <code>{actual}</code>与基本 url <code>{configured}</code> 不匹配。 diff --git a/backend/src/Squidex.Web/ApiController.cs b/backend/src/Squidex.Web/ApiController.cs index edbfd6ec5..4cb86cd7b 100644 --- a/backend/src/Squidex.Web/ApiController.cs +++ b/backend/src/Squidex.Web/ApiController.cs @@ -9,8 +9,12 @@ using Microsoft.AspNetCore.Mvc; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Teams; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Translations; +using Squidex.Shared; namespace Squidex.Web { @@ -41,6 +45,22 @@ namespace Squidex.Web } } + protected ITeamEntity Team + { + get + { + var team = HttpContext.Features.Get()?.Team; + + if (team == null) + { + ThrowHelper.InvalidOperationException("Not in a team context."); + return default!; + } + + return team; + } + } + protected ISchemaEntity Schema { get @@ -57,6 +77,31 @@ namespace Squidex.Web } } + protected string UserId + { + get + { + var subject = User.OpenIdSubject(); + + if (string.IsNullOrWhiteSpace(subject)) + { + throw new DomainForbiddenException(T.Get("common.httpOnlyAsUser")); + } + + return subject; + } + } + + protected bool IsFrontend + { + get => HttpContext.User.IsInClient(DefaultClients.Frontend); + } + + protected string UserOrClientId + { + get => HttpContext.User.UserOrClientId()!; + } + protected Resources Resources { get => resources.Value; @@ -72,6 +117,11 @@ namespace Squidex.Web get => App.Id; } + protected DomainId TeamId + { + get => Team.Id; + } + protected ApiController(ICommandBus commandBus) { CommandBus = commandBus; diff --git a/backend/src/Squidex.Web/ApiPermissionAttribute.cs b/backend/src/Squidex.Web/ApiPermissionAttribute.cs index f7eb6152e..3ba03cdfa 100644 --- a/backend/src/Squidex.Web/ApiPermissionAttribute.cs +++ b/backend/src/Squidex.Web/ApiPermissionAttribute.cs @@ -55,7 +55,14 @@ namespace Squidex.Web schema = Permission.Any; } - if (permissions.Allows(id, app, schema)) + var team = context.HttpContext.Features.Get()?.Team.Id.ToString(); + + if (string.IsNullOrWhiteSpace(team)) + { + team = Permission.Any; + } + + if (permissions.Allows(id, app, schema, team)) { hasPermission = true; break; diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithTeamIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithTeamIdCommandMiddleware.cs new file mode 100644 index 000000000..c74ad539c --- /dev/null +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithTeamIdCommandMiddleware.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Entities; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Web.CommandMiddlewares +{ + public sealed class EnrichWithTeamIdCommandMiddleware : ICommandMiddleware + { + private readonly IHttpContextAccessor httpContextAccessor; + + public EnrichWithTeamIdCommandMiddleware(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (httpContextAccessor.HttpContext == null) + { + return next(context, ct); + } + + if (context.Command is ITeamCommand teamCommand && teamCommand.TeamId == default) + { + var teamId = GetTeamId(); + + teamCommand.TeamId = teamId; + } + + return next(context, ct); + } + + private DomainId GetTeamId() + { + var feature = httpContextAccessor.HttpContext?.Features.Get(); + + if (feature == null) + { + ThrowHelper.InvalidOperationException("Cannot resolve team."); + return default!; + } + + return feature.Team.Id; + } + } +} diff --git a/backend/src/Squidex.Web/ContextExtensions.cs b/backend/src/Squidex.Web/ContextExtensions.cs index 7c7f812cd..be499e1be 100644 --- a/backend/src/Squidex.Web/ContextExtensions.cs +++ b/backend/src/Squidex.Web/ContextExtensions.cs @@ -18,12 +18,26 @@ namespace Squidex.Web if (context == null) { - context = RequestContext.Anonymous(null!); + context = new RequestContext(httpContext.User, null!).WithHeaders(httpContext); httpContext.Features.Set(context); } return context; } + + public static RequestContext WithHeaders(this RequestContext context, HttpContext httpContext) + { + return context.Clone(builder => + { + foreach (var (key, value) in httpContext.Request.Headers) + { + if (key.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) + { + builder.SetHeader(key, value.ToString()); + } + } + }); + } } } diff --git a/backend/src/Squidex.Web/ITeamFeature.cs b/backend/src/Squidex.Web/ITeamFeature.cs new file mode 100644 index 000000000..aff13155b --- /dev/null +++ b/backend/src/Squidex.Web/ITeamFeature.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Teams; + +namespace Squidex.Web +{ + public interface ITeamFeature + { + ITeamEntity Team { get; } + } +} diff --git a/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs index dc48b80d8..fd8f62436 100644 --- a/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs @@ -8,18 +8,18 @@ using System.Globalization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure; namespace Squidex.Web.Pipeline { public sealed class ApiCostsFilter : IAsyncActionFilter, IFilterContainer { - private readonly UsageGate usageGate; + private readonly IAppUsageGate appUsageGate; - public ApiCostsFilter(UsageGate usageGate) + public ApiCostsFilter(IAppUsageGate appUsageGate) { - this.usageGate = usageGate; + this.appUsageGate = appUsageGate; } IFilterMetadata IFilterContainer.FilterDefinition { get; set; } @@ -50,7 +50,7 @@ namespace Squidex.Web.Pipeline { var (_, clientId) = context.HttpContext.User.GetClient(); - var isBlocked = await usageGate.IsBlockedAsync(app, clientId, DateTime.Today, context.HttpContext.RequestAborted); + var isBlocked = await appUsageGate.IsBlockedAsync(app, clientId, DateTime.Today, context.HttpContext.RequestAborted); if (isBlocked) { diff --git a/backend/src/Squidex.Web/Pipeline/AppResolver.cs b/backend/src/Squidex.Web/Pipeline/AppResolver.cs index a6653298d..5468d0a06 100644 --- a/backend/src/Squidex.Web/Pipeline/AppResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/AppResolver.cs @@ -7,7 +7,6 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; @@ -91,7 +90,7 @@ namespace Squidex.Web.Pipeline } } - var requestContext = SetContext(context.HttpContext, app); + var requestContext = new Context(context.HttpContext.User, app).WithHeaders(context.HttpContext); if (!AllowAnonymous(context) && !HasPermission(appName, requestContext)) { @@ -113,36 +112,14 @@ namespace Squidex.Web.Pipeline return; } + context.HttpContext.Features.Set(requestContext); context.HttpContext.Features.Set(new AppFeature(app)); context.HttpContext.Response.Headers.Add("X-AppId", app.Id.ToString()); } - else - { - SetContext(context.HttpContext, null!); - } await next(); } - private static Context SetContext(HttpContext httpContext, IAppEntity app) - { - var requestContext = - new Context(httpContext.User, app).Clone(builder => - { - foreach (var (key, value) in httpContext.Request.Headers) - { - if (key.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) - { - builder.SetHeader(key, value.ToString()); - } - } - }); - - httpContext.Features.Set(requestContext); - - return requestContext; - } - private static bool HasPermission(string appName, Context requestContext) { return requestContext.UserPermissions.Includes(PermissionIds.ForApp(PermissionIds.App, appName)); diff --git a/backend/src/Squidex.Web/Pipeline/ContextFilter.cs b/backend/src/Squidex.Web/Pipeline/ContextFilter.cs new file mode 100644 index 000000000..9876d31a6 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/ContextFilter.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Domain.Apps.Entities; + +namespace Squidex.Web.Pipeline +{ + public sealed class ContextFilter : IAsyncActionFilter + { + public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var httpContext = context.HttpContext; + + var requestContext = + new Context(httpContext.User, null!).Clone(builder => + { + foreach (var (key, value) in httpContext.Request.Headers) + { + if (key.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) + { + builder.SetHeader(key, value.ToString()); + } + } + }); + + httpContext.Features.Set(requestContext); + + return next(); + } + } +} diff --git a/backend/src/Squidex.Web/Pipeline/TeamFeature.cs b/backend/src/Squidex.Web/Pipeline/TeamFeature.cs new file mode 100644 index 000000000..9fecc7131 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/TeamFeature.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Teams; + +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + +namespace Squidex.Web.Pipeline +{ + public sealed record TeamFeature(ITeamEntity Team) : ITeamFeature; +} diff --git a/backend/src/Squidex.Web/Pipeline/TeamResolver.cs b/backend/src/Squidex.Web/Pipeline/TeamResolver.cs new file mode 100644 index 000000000..42c86a908 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/TeamResolver.cs @@ -0,0 +1,105 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Squidex.Domain.Apps.Entities; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Shared.Identity; + +namespace Squidex.Web.Pipeline +{ + public sealed class TeamResolver : IAsyncActionFilter + { + private readonly IAppProvider appProvider; + + public TeamResolver(IAppProvider appProvider) + { + this.appProvider = appProvider; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var user = context.HttpContext.User; + + if (context.RouteData.Values.TryGetValue("team", out var teamValue)) + { + var teamId = teamValue?.ToString(); + + if (string.IsNullOrWhiteSpace(teamId)) + { + context.Result = new NotFoundResult(); + return; + } + + var team = await appProvider.GetTeamAsync(DomainId.Create(teamId), default); + + if (team == null) + { + var log = context.HttpContext.RequestServices?.GetService>(); + + log?.LogWarning("Cannot find team with the given id {id}.", teamId); + + context.Result = new NotFoundResult(); + return; + } + + var subjectId = user.OpenIdSubject(); + + if (subjectId != null && team.Contributors.TryGetValue(subjectId, out var role)) + { + var identity = user.Identities.First(); + + identity.AddClaim(new Claim(ClaimTypes.Role, role)); + identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.ForApp(PermissionIds.TeamAdmin, team: team.Id.ToString()).Id)); + } + + var requestContext = new Context(context.HttpContext.User, null!).WithHeaders(context.HttpContext); + + if (!AllowAnonymous(context) && !HasPermission(team.Id, requestContext)) + { + if (string.IsNullOrWhiteSpace(user.Identity?.AuthenticationType)) + { + context.Result = new UnauthorizedResult(); + } + else + { + var log = context.HttpContext.RequestServices?.GetService>(); + + log?.LogWarning("Authenticated user has no permission to access the team with ID {id}.", team.Id); + + context.Result = new NotFoundResult(); + } + + return; + } + + context.HttpContext.Features.Set(requestContext); + context.HttpContext.Features.Set(new TeamFeature(team)); + context.HttpContext.Response.Headers.Add("X-TeamId", team.Id.ToString()); + } + + await next(); + } + + private static bool HasPermission(DomainId teamId, Context requestContext) + { + return requestContext.UserPermissions.Includes(PermissionIds.ForApp(PermissionIds.Team, team: teamId.ToString())); + } + + private static bool AllowAnonymous(ActionExecutingContext context) + { + return context.ActionDescriptor.EndpointMetadata.Any(x => x is AllowAnonymousAttribute); + } + } +} diff --git a/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs b/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs index 0a2260df2..b2d55c56c 100644 --- a/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs @@ -9,23 +9,23 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using NodaTime; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure; using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.UsageTracking; namespace Squidex.Web.Pipeline { public sealed class UsageMiddleware : IMiddleware { - private readonly IAppLogStore usageLog; - private readonly IApiUsageTracker usageTracker; + private readonly IAppLogStore appUsageLog; + private readonly IAppUsageGate appUsageGate; public IClock Clock { get; set; } = SystemClock.Instance; - public UsageMiddleware(IAppLogStore usageLog, IApiUsageTracker usageTracker ) + public UsageMiddleware(IAppLogStore appUsageLog, IAppUsageGate appUsageGate) { - this.usageLog = usageLog; - this.usageTracker = usageTracker; + this.appUsageLog = appUsageLog; + this.appUsageGate = appUsageGate; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) @@ -41,9 +41,9 @@ namespace Squidex.Web.Pipeline { if (context.Response.StatusCode != StatusCodes.Status429TooManyRequests) { - var appId = context.Features.Get()?.App.Id; + var app = context.Features.Get()?.App; - if (appId != null) + if (app != null) { var bytes = usageBody.BytesWritten; @@ -69,14 +69,13 @@ namespace Squidex.Web.Pipeline request.UserClientId = clientId; // Do not flow cancellation token because it is too important. - await usageLog.LogAsync(appId.Value, request, default); + await appUsageLog.LogAsync(app.Id, request, default); if (request.Costs > 0) { var date = request.Timestamp.ToDateTimeUtc().Date; - await usageTracker.TrackAsync(date, appId.Value.ToString(), - request.UserClientId, + await appUsageGate.TrackRequestAsync(app, request.UserClientId, date, request.Costs, request.ElapsedMs, request.Bytes, diff --git a/backend/src/Squidex.Web/Resources.cs b/backend/src/Squidex.Web/Resources.cs index a6b814aa5..1721b78e7 100644 --- a/backend/src/Squidex.Web/Resources.cs +++ b/backend/src/Squidex.Web/Resources.cs @@ -52,8 +52,12 @@ namespace Squidex.Web // Contributors public bool CanAssignContributor => Can(PermissionIds.AppContributorsAssign); + public bool CanAssignTeamContributor => Can(PermissionIds.TeamContributorsAssign); + public bool CanRevokeContributor => Can(PermissionIds.AppContributorsRevoke); + public bool CanRevokeTeamContributor => Can(PermissionIds.TeamContributorsRevoke); + // Workflows public bool CanCreateWorkflow => Can(PermissionIds.AppWorkflowsCreate); @@ -141,6 +145,8 @@ namespace Squidex.Web public string? Schema => GetAppName(); + public string? Team => GetTeamId().ToString(); + public DomainId AppId => GetAppId(); public ApiController Controller { get; } @@ -187,7 +193,7 @@ namespace Squidex.Web 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) + public bool IsAllowed(string id, string app = Permission.Any, string schema = Permission.Any, string team = Permission.Any, PermissionSet? additional = null) { if (app == Permission.Any) { @@ -209,7 +215,17 @@ namespace Squidex.Web } } - var permission = PermissionIds.ForApp(id, app, schema); + if (team == Permission.Any) + { + var fallback = GetTeamId(); + + if (fallback != default) + { + team = fallback.ToString(); + } + } + + var permission = PermissionIds.ForApp(id, app, schema, team); return Context.UserPermissions.Allows(permission) || additional?.Allows(permission) == true; } @@ -228,5 +244,10 @@ namespace Squidex.Web { return Controller.HttpContext.Context().App?.Id ?? default; } + + private DomainId GetTeamId() + { + return Controller.HttpContext.Features.Get()?.Team?.Id ?? default; + } } } diff --git a/backend/src/Squidex.Web/UsageOptions.cs b/backend/src/Squidex.Web/UsageOptions.cs index 7b4e5dc80..0a001710c 100644 --- a/backend/src/Squidex.Web/UsageOptions.cs +++ b/backend/src/Squidex.Web/UsageOptions.cs @@ -5,12 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; namespace Squidex.Web { public sealed class UsageOptions { - public ConfigAppLimitsPlan[] Plans { get; set; } + public Plan[] Plans { get; set; } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 781ef6349..8191db081 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -87,7 +87,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// Updates an app client. /// /// The name of the app. - /// The id of the client that must be updated. + /// The ID of the client that must be updated. /// Client object that needs to be updated. /// /// 200 => Client updated. @@ -115,7 +115,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// Revoke an app client. /// /// The name of the app. - /// The id of the client that must be deleted. + /// The ID of the client that must be deleted. /// /// 200 => Client deleted. /// 404 => Client or app not found. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index bc27eaa9f..24483ed83 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -7,15 +7,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; -using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Invitation; -using Squidex.Domain.Apps.Entities.Apps.Plans; -using Squidex.Infrastructure; +using Squidex.Domain.Apps.Entities.Billing; +using Squidex.Domain.Apps.Entities.Invitation; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Translations; +using Squidex.Infrastructure.Reflection; using Squidex.Shared; using Squidex.Shared.Users; using Squidex.Web; @@ -28,15 +25,14 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiExplorerSettings(GroupName = nameof(Apps))] public sealed class AppContributorsController : ApiController { - private readonly IAppPlansProvider appPlansProvider; - private readonly IUserResolver userResolver; + private readonly IAppUsageGate usageTracker; + private readonly IUserResolver usageGate; - public AppContributorsController(ICommandBus commandBus, IAppPlansProvider appPlansProvider, IUserResolver userResolver) + public AppContributorsController(ICommandBus commandBus, IAppUsageGate usageGate, IUserResolver userResolver) : base(commandBus) { - this.appPlansProvider = appPlansProvider; - - this.userResolver = userResolver; + this.usageTracker = usageGate; + this.usageGate = userResolver; } /// @@ -81,7 +77,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiCosts(1)] public async Task PostContributor(string app, [FromBody] AssignContributorDto request) { - var command = request.ToCommand(); + var command = SimpleMapper.Map(request, new AssignContributor()); var response = await InvokeCommandAsync(command); @@ -103,7 +99,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiCosts(1)] public async Task DeleteMyself(string app) { - var command = new RemoveContributor { ContributorId = UserId() }; + var command = new RemoveContributor { ContributorId = UserId }; var response = await InvokeCommandAsync(command); @@ -114,7 +110,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// Remove contributor. /// /// The name of the app. - /// The id of the contributor. + /// The ID of the contributor. /// /// 200 => Contributor removed. /// 404 => Contributor or app not found. @@ -137,9 +133,9 @@ namespace Squidex.Areas.Api.Controllers.Apps { var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - if (context.PlainResult is InvitedResult invited) + if (context.PlainResult is InvitedResult invited) { - return await GetResponseAsync(invited.App, true); + return await GetResponseAsync(invited.Entity, true); } else { @@ -147,21 +143,11 @@ namespace Squidex.Areas.Api.Controllers.Apps } } - private string UserId() + private async Task GetResponseAsync(IAppEntity app, bool invited) { - var subject = User.OpenIdSubject(); - - if (string.IsNullOrWhiteSpace(subject)) - { - throw new DomainForbiddenException(T.Get("common.httpOnlyAsUser")); - } + var (plan, _, _) = await usageTracker.GetPlanForAppAsync(app, HttpContext.RequestAborted); - return subject; - } - - private Task GetResponseAsync(IAppEntity app, bool invited) - { - return ContributorsDto.FromAppAsync(app, Resources, userResolver, appPlansProvider, invited); + return await ContributorsDto.FromDomainAsync(app, Resources, usageGate, plan, invited); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs index d51c40c78..c8bc747d2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs @@ -85,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// Update a workflow. /// /// The name of the app. - /// The id of the workflow to update. + /// The ID of the workflow to update. /// The new workflow. /// /// 200 => Workflow updated. @@ -110,7 +110,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// Delete a workflow. /// /// The name of the app. - /// The id of the workflow to update. + /// The ID of the workflow to update. /// /// 200 => Workflow deleted. /// 404 => Workflow or app not found. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 97f666614..577f42f21 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -51,16 +51,44 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiCosts(0)] public async Task GetApps() { - var userOrClientId = HttpContext.User.UserOrClientId()!; + var userOrClientId = UserOrClientId!; var userPermissions = Resources.Context.UserPermissions; var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions, HttpContext.RequestAborted); var response = Deferred.Response(() => { - var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend); + return apps.OrderBy(x => x.Name).Select(a => AppDto.FromDomain(a, userOrClientId, IsFrontend, Resources)).ToArray(); + }); + + Response.Headers[HeaderNames.ETag] = apps.ToEtag(); + + return Ok(response); + } + + /// + /// Get team apps. + /// + /// The ID of the team. + /// + /// 200 => Apps returned. + /// + /// + /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). + /// You will retrieve all apps, where you are assigned as a contributor. + /// + [HttpGet] + [Route("teams/{team}/apps")] + [ProducesResponseType(typeof(AppDto[]), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(0)] + public async Task GetTeamApps(string team) + { + var apps = await appProvider.GetTeamAppsAsync(Team.Id, HttpContext.RequestAborted); - return apps.OrderBy(x => x.Name).Select(a => AppDto.FromDomain(a, userOrClientId, isFrontend, Resources)).ToArray(); + var response = Deferred.Response(() => + { + return apps.OrderBy(x => x.Name).Select(a => AppDto.FromDomain(a, UserOrClientId, IsFrontend, Resources)).ToArray(); }); Response.Headers[HeaderNames.ETag] = apps.ToEtag(); @@ -85,11 +113,9 @@ namespace Squidex.Areas.Api.Controllers.Apps { var response = Deferred.Response(() => { - var userOrClientId = HttpContext.User.UserOrClientId()!; - var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend); - return AppDto.FromDomain(App, userOrClientId, isFrontend, Resources); + return AppDto.FromDomain(App, UserOrClientId, IsFrontend, Resources); }); Response.Headers[HeaderNames.ETag] = App.ToEtag(); @@ -144,6 +170,28 @@ namespace Squidex.Areas.Api.Controllers.Apps return Ok(response); } + /// + /// Transfer the app. + /// + /// The name of the app to update. + /// The team information. + /// + /// 200 => App transferred. + /// 400 => App request not valid. + /// 404 => App not found. + /// + [HttpPut] + [Route("apps/{app}/team")] + [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppTransfer)] + [ApiCosts(0)] + public async Task PutAppTeam(string app, [FromBody] TransferToTeamDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); + + return Ok(response); + } + /// /// Upload the app image. /// @@ -211,11 +259,7 @@ namespace Squidex.Areas.Api.Controllers.Apps { return InvokeCommandAsync(command, x => { - var userOrClientId = HttpContext.User.UserOrClientId()!; - - var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend); - - return AppDto.FromDomain(x, userOrClientId, isFrontend, Resources); + return AppDto.FromDomain(x, UserOrClientId, IsFrontend, Resources); }); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index 5610627bb..69846ae84 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -28,6 +28,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class AppDto : Resource { + /// + /// The ID of the app. + /// + public DomainId Id { get; set; } + /// /// The name of the app. /// @@ -50,11 +55,6 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// public long Version { get; set; } - /// - /// The id of the app. - /// - public DomainId Id { get; set; } - /// /// The timestamp when the app has been created. /// @@ -65,6 +65,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// public Instant LastModified { get; set; } + /// + /// The ID of the team. + /// + public DomainId? TeamId { get; set; } + /// /// The permission level of the user. /// @@ -123,7 +128,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models result.RoleProperties = new JsonObject(); } - foreach (var (key, value) in resources.Context.User.Claims.GetUIProperties(app.Name)) + foreach (var (key, value) in resources.Context.UserPrincipal.Claims.GetUIProperties(app.Name)) { result.RoleProperties[key] = JsonValue.Create(value); } @@ -161,6 +166,12 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models resources.Url(x => nameof(x.DeleteApp), values)); } + if (resources.IsAllowed(PermissionIds.AppTransfer, Name, additional: permissions)) + { + AddPutLink("transfer", + resources.Url(x => nameof(x.PutAppTeam), values)); + } + if (resources.IsAllowed(PermissionIds.AppUpdate, Name, additional: permissions)) { AddPutLink("update", diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs index b1911f6a4..d4fc227da 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs @@ -14,7 +14,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models public sealed class CreateClientDto { /// - /// The id of the client. + /// The ID of the client. /// [LocalizedRequired] [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/TransferToTeamDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/TransferToTeamDto.cs new file mode 100644 index 000000000..f69aceb47 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/TransferToTeamDto.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class TransferToTeamDto + { + /// + /// The ID of the team. + /// + public DomainId? TeamId { get; set; } + + public TransferToTeam ToCommand() + { + return SimpleMapper.Map(this, new TransferToTeam()); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 35cdc13f6..7ec783cd4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -79,7 +79,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// /// Get the asset content. /// - /// The id of the asset. + /// The ID of the asset. /// The request parameters. /// /// 200 => Asset found and content or (resized) image returned. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs index ac9317383..d8353dc3e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs @@ -96,7 +96,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// Update an asset folder. /// /// The name of the app. - /// The id of the asset folder. + /// The ID of the asset folder. /// The asset folder object that needs to updated. /// /// 200 => Asset folder updated. @@ -122,7 +122,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// Move an asset folder. /// /// The name of the app. - /// The id of the asset folder. + /// The ID of the asset folder. /// The asset folder object that needs to updated. /// /// 200 => Asset folder moved. @@ -148,7 +148,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// Delete an asset folder. /// /// The name of the app. - /// The id of the asset folder to delete. + /// The ID of the asset folder to delete. /// /// 204 => Asset folder deleted. /// 404 => Asset folder or app not found. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 2f0c62617..bfc904bd8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -14,9 +14,9 @@ using Squidex.Assets; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Translations; @@ -32,24 +32,24 @@ namespace Squidex.Areas.Api.Controllers.Assets [ApiExplorerSettings(GroupName = nameof(Assets))] public sealed class AssetsController : ApiController { + private readonly IAppUsageGate appUsageGate; private readonly IAssetQueryService assetQuery; - private readonly IAssetUsageTracker assetStatsRepository; - private readonly IAppPlansProvider appPlansProvider; + private readonly IAssetUsageTracker assetUsageTracker; private readonly ITagService tagService; private readonly AssetTusRunner assetTusRunner; public AssetsController( ICommandBus commandBus, + IAppUsageGate appUsageGate, IAssetQueryService assetQuery, - IAssetUsageTracker assetStatsRepository, - IAppPlansProvider appPlansProvider, + IAssetUsageTracker assetUsageTracker, ITagService tagService, AssetTusRunner assetTusRunner) : base(commandBus) { - this.appPlansProvider = appPlansProvider; + this.appUsageGate = appUsageGate; this.assetQuery = assetQuery; - this.assetStatsRepository = assetStatsRepository; + this.assetUsageTracker = assetUsageTracker; this.assetTusRunner = assetTusRunner; this.tagService = tagService; } @@ -165,7 +165,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// Get an asset by id. /// /// The name of the app. - /// The id of the asset to retrieve. + /// The ID of the asset to retrieve. /// /// 200 => Asset found. /// 404 => Asset or app not found. @@ -319,7 +319,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// Replace asset content. /// /// The name of the app. - /// The id of the asset. + /// The ID of the asset. /// The file to upload. /// /// 200 => Asset updated. @@ -349,7 +349,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// Update an asset. /// /// The name of the app. - /// The id of the asset. + /// The ID of the asset. /// The asset object that needs to updated. /// /// 200 => Asset updated. @@ -375,7 +375,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// Moves the asset. /// /// The name of the app. - /// The id of the asset. + /// The ID of the asset. /// The asset object that needs to updated. /// /// 200 => Asset moved. @@ -401,7 +401,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// Delete an asset. /// /// The name of the app. - /// The id of the asset to delete. + /// The ID of the asset to delete. /// The request parameters. /// /// 204 => Asset deleted. @@ -469,9 +469,9 @@ namespace Squidex.Areas.Api.Controllers.Assets throw new ValidationException(error); } - var (plan, _) = appPlansProvider.GetPlanForApp(App); + var (plan, _, _) = await appUsageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); - var currentSize = await assetStatsRepository.GetTotalSizeAsync(AppId); + var currentSize = await assetUsageTracker.GetTotalSizeByAppAsync(AppId, HttpContext.RequestAborted); if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + file.Length) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index d2873301c..616c742e7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -19,12 +19,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models public sealed class AssetDto : Resource { /// - /// The id of the asset. + /// The ID of the asset. /// public DomainId Id { get; set; } /// - /// The id of the parent folder. Empty for files without parent. + /// The ID of the parent folder. Empty for files without parent. /// public DomainId ParentId { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs index 3664a4262..da5b02f95 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs @@ -16,12 +16,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models public sealed class AssetFolderDto : Resource { /// - /// The id of the asset. + /// The ID of the asset. /// public DomainId Id { get; set; } /// - /// The id of the parent folder. Empty for files without parent. + /// The ID of the parent folder. Empty for files without parent. /// public DomainId ParentId { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs index 3e5bd6d1b..b1ead93c1 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs @@ -15,7 +15,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models public class BulkUpdateAssetsJobDto { /// - /// An optional id of the asset to update. + /// An optional ID of the asset to update. /// public DomainId Id { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs index 52afd4dab..f1f18987a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs @@ -21,7 +21,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models public string FolderName { get; set; } /// - /// The id of the parent folder. + /// The ID of the parent folder. /// public DomainId ParentId { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs b/backend/src/Squidex/Areas/Api/Controllers/AssignContributorDto.cs similarity index 78% rename from backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/AssignContributorDto.cs index 64f8be105..88cbaf143 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/AssignContributorDto.cs @@ -5,12 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; using Roles = Squidex.Domain.Apps.Core.Apps.Role; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers { public sealed class AssignContributorDto { @@ -29,10 +27,5 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// Set to true to invite the user if he does not exist. /// public bool Invite { get; set; } - - public AssignContributor ToCommand() - { - return SimpleMapper.Map(this, new AssignContributor()); - } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs index 9be09dd94..2e66fbf73 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs @@ -36,7 +36,7 @@ namespace Squidex.Areas.Api.Controllers.Backups /// Get the backup content. /// /// The name of the app. - /// The id of the backup. + /// The ID of the backup. /// /// 200 => Backup found and content returned. /// 404 => Backup or app not found. @@ -55,8 +55,8 @@ namespace Squidex.Areas.Api.Controllers.Backups /// /// Get the backup content. /// - /// The id of the backup. - /// The id of the app. + /// The ID of the backup. + /// The ID of the app. /// The name of the app. /// /// 200 => Backup found and content returned. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs index 27ca1a44c..547cbe112 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs @@ -77,7 +77,7 @@ namespace Squidex.Areas.Api.Controllers.Backups /// Delete a backup. /// /// The name of the app. - /// The id of the backup to delete. + /// The ID of the backup to delete. /// /// 204 => Backup deleted. /// 404 => Backup or app not found. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs index 17aedbf9a..323aecb35 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs @@ -16,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models public sealed class BackupJobDto : Resource { /// - /// The id of the backup job. + /// The ID of the backup job. /// public DomainId Id { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/BulkResultDto.cs b/backend/src/Squidex/Areas/Api/Controllers/BulkResultDto.cs index a6bc6df8a..ea7dce662 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/BulkResultDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/BulkResultDto.cs @@ -25,12 +25,12 @@ namespace Squidex.Areas.Api.Controllers public int JobIndex { get; set; } /// - /// The id of the entity that has been handled successfully or not. + /// The ID of the entity that has been handled successfully or not. /// public DomainId? Id { get; set; } /// - /// The id of the entity that has been handled successfully or not. + /// The ID of the entity that has been handled successfully or not. /// [Obsolete("Use 'id' field now.")] public DomainId? ContentId => Id; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs index bedc8ece7..0182a6284 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs @@ -13,8 +13,6 @@ using Squidex.Domain.Apps.Entities.Comments; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Translations; using Squidex.Shared; using Squidex.Web; @@ -54,7 +52,7 @@ namespace Squidex.Areas.Api.Controllers.Comments [ApiCosts(0)] public async Task GetWatchingUsers(string app, string? resource = null) { - var result = await watchingService.GetWatchingUsersAsync(App.Id, resource, UserId(), HttpContext.RequestAborted); + var result = await watchingService.GetWatchingUsersAsync(App.Id, resource, UserId, HttpContext.RequestAborted); return Ok(result); } @@ -63,7 +61,7 @@ namespace Squidex.Areas.Api.Controllers.Comments /// Get all comments. /// /// The name of the app. - /// The id of the comments. + /// The ID of the comments. /// The current version. /// /// When passing in a version you can retrieve all updates since then. @@ -95,7 +93,7 @@ namespace Squidex.Areas.Api.Controllers.Comments /// Create a new comment. /// /// The name of the app. - /// The id of the comments. + /// The ID of the comments. /// The comment object that needs to created. /// /// 201 => Comment created. @@ -122,8 +120,8 @@ namespace Squidex.Areas.Api.Controllers.Comments /// Update a comment. /// /// The name of the app. - /// The id of the comments. - /// The id of the comment. + /// The ID of the comments. + /// The ID of the comment. /// The comment object that needs to updated. /// /// 204 => Comment updated. @@ -147,8 +145,8 @@ namespace Squidex.Areas.Api.Controllers.Comments /// Delete a comment. /// /// The name of the app. - /// The id of the comments. - /// The id of the comment. + /// The ID of the comments. + /// The ID of the comment. /// /// 204 => Comment deleted. /// 404 => Comment or app not found. @@ -174,17 +172,5 @@ namespace Squidex.Areas.Api.Controllers.Comments { return DomainId.Combine(App.Id, commentsId); } - - private string UserId() - { - var subject = User.OpenIdSubject(); - - if (string.IsNullOrWhiteSpace(subject)) - { - throw new DomainForbiddenException(T.Get("common.httpOnlyAsUser")); - } - - return subject; - } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs index 210a2bbc6..60a2eb5f5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs @@ -17,7 +17,7 @@ namespace Squidex.Areas.Api.Controllers.Comments.Models public sealed class CommentDto { /// - /// The id of the comment. + /// The ID of the comment. /// public DomainId Id { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs index 548fecc35..9446cffe6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs @@ -68,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Comments.Notifications /// Delete a notification. /// /// The user id. - /// The id of the comment. + /// The ID of the comment. /// /// 204 => Comment deleted. /// 404 => Comment not found. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 1c8553ea7..f219b2fdd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -184,7 +184,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content to fetch. + /// The ID of the content to fetch. /// /// 200 => Content returned. /// 404 => Content, schema or app not found. @@ -219,7 +219,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content to fetch. + /// The ID of the content to fetch. /// /// 204 => Content is valid. /// 400 => Content not valid. @@ -246,7 +246,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content to fetch. + /// The ID of the content to fetch. /// The optional json query. /// /// 200 => Contents returned. @@ -277,7 +277,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content to fetch. + /// The ID of the content to fetch. /// The optional json query. /// /// 200 => Content returned. @@ -308,7 +308,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content to fetch. + /// The ID of the content to fetch. /// The version fo the content to fetch. /// /// 200 => Content version returned. @@ -431,7 +431,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content item to update. + /// The ID of the content item to update. /// The request parameters. /// /// 200 => Content created or updated. @@ -460,7 +460,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content item to update. + /// The ID of the content item to update. /// The full data for the content item. /// /// 200 => Content updated. @@ -489,7 +489,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content item to patch. + /// The ID of the content item to patch. /// The patch for the content item. /// /// 200 => Content patched. @@ -518,7 +518,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content item to change. + /// The ID of the content item to change. /// The status request. /// /// 200 => Content status changed. @@ -547,7 +547,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content item to cancel. + /// The ID of the content item to cancel. /// /// 200 => Content status change cancelled. /// 400 => Content request not valid. @@ -575,7 +575,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content item to create the draft for. + /// The ID of the content item to create the draft for. /// /// 200 => Content draft created. /// 404 => Content, schema or app not found. @@ -602,7 +602,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content item to delete the draft from. + /// The ID of the content item to delete the draft from. /// /// 200 => Content draft deleted. /// 404 => Content, schema or app not found. @@ -629,7 +629,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// /// The name of the app. /// The name of the schema. - /// The id of the content item to delete. + /// The ID of the content item to delete. /// The request parameters. /// /// 204 => Content deleted. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs index f328c0f81..b9d00d4d9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs @@ -21,7 +21,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models public QueryJsonDto? Query { get; set; } /// - /// An optional id of the content to update. + /// An optional ID of the content to update. /// public DomainId? Id { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index 0aee184e8..dce83f2ee 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -88,7 +88,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models public ScheduleJobDto? ScheduleJob { get; set; } /// - /// The id of the schema. + /// The ID of the schema. /// public DomainId SchemaId { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index cbd9e4a0c..79e08087e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -69,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models AddPostLink("create", resources.Url(x => nameof(x.PostContent), values)); - if (resources.CanChangeStatus(values.schema) && await workflow.CanPublishInitialAsync(schema, resources.Context.User)) + if (resources.CanChangeStatus(values.schema) && await workflow.CanPublishInitialAsync(schema, resources.Context.UserPrincipal)) { var publishValues = new { values.app, values.schema, publish = true }; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs index e2fea0d0b..b50b178d5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs @@ -15,7 +15,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models public sealed class ScheduleJobDto { /// - /// The id of the schedule job. + /// The ID of the schedule job. /// public DomainId Id { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs b/backend/src/Squidex/Areas/Api/Controllers/ContributorDto.cs similarity index 52% rename from backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/ContributorDto.cs index fbf7259ed..57772ec80 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/ContributorDto.cs @@ -5,18 +5,20 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Areas.Api.Controllers.Apps; +using Squidex.Areas.Api.Controllers.Teams; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; using Squidex.Shared.Identity; using Squidex.Shared.Users; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers { public sealed class ContributorDto : Resource { /// - /// The id of the user that contributes to the app. + /// The ID of the user that contributes to the app. /// [LocalizedRequired] public string ContributorId { get; set; } @@ -60,27 +62,57 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models return this; } - public ContributorDto CreateLinks(Resources resources) + public ContributorDto CreateAppLinks(Resources resources) { - if (!resources.IsUser(ContributorId)) + if (resources.IsUser(ContributorId)) { - var app = resources.App; + return this; + } + + var app = resources.App; + + if (resources.CanAssignContributor) + { + var values = new { app }; - if (resources.CanAssignContributor) - { - var values = new { app }; + AddPostLink("update", + resources.Url(x => nameof(x.PostContributor), values)); + } - AddPostLink("update", - resources.Url(x => nameof(x.PostContributor), values)); - } + if (resources.CanRevokeContributor) + { + var values = new { app, id = ContributorId }; + + AddDeleteLink("delete", + resources.Url(x => nameof(x.DeleteContributor), values)); + } + + return this; + } - if (resources.CanRevokeContributor) - { - var values = new { app, id = ContributorId }; + public ContributorDto CreateTeamLinks(Resources resources) + { + if (resources.IsUser(ContributorId)) + { + return this; + } + + var team = resources.Team; + + if (resources.CanAssignTeamContributor) + { + var values = new { team }; + + AddPostLink("update", + resources.Url(x => nameof(x.PostContributor), values)); + } + + if (resources.CanRevokeTeamContributor) + { + var values = new { team, id = ContributorId }; - AddDeleteLink("delete", - resources.Url(x => nameof(x.DeleteContributor), values)); - } + AddDeleteLink("delete", + resources.Url(x => nameof(x.DeleteContributor), values)); } return this; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/ContributorsDto.cs similarity index 53% rename from backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/ContributorsDto.cs index 748503e5a..f2532f767 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/ContributorsDto.cs @@ -6,13 +6,16 @@ // ========================================================================== using System.Text.Json.Serialization; +using Squidex.Areas.Api.Controllers.Apps; +using Squidex.Areas.Api.Controllers.Teams; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; +using Squidex.Domain.Apps.Entities.Teams; using Squidex.Infrastructure.Validation; using Squidex.Shared.Users; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers { public sealed class ContributorsDto : Resource { @@ -25,7 +28,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// /// The maximum number of allowed contributors. /// - public int MaxContributors { get; set; } + public long MaxContributors { get; set; } /// /// The metadata to provide information about this request. @@ -33,7 +36,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models [JsonPropertyName("_meta")] public ContributorsMetadata? Metadata { get; set; } - public static async Task FromAppAsync(IAppEntity app, Resources resources, IUserResolver userResolver, IAppPlansProvider plans, bool invited) + public static async Task FromDomainAsync(IAppEntity app, Resources resources, IUserResolver userResolver, Plan plan, bool invited) { var users = await userResolver.QueryManyAsync(app.Contributors.Keys.ToArray()); @@ -42,20 +45,39 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models Items = app.Contributors .Select(x => ContributorDto.FromDomain(x.Key, x.Value)) .Select(x => x.CreateUser(users)) - .Select(x => x.CreateLinks(resources)) + .Select(x => x.CreateAppLinks(resources)) .OrderBy(x => x.ContributorName) .ToArray() }; result.CreateInvited(invited); - result.CreatePlan(app, plans); + result.CreatePlan(plan); - return result.CreateLinks(resources); + return result.CreateAppLinks(resources); } - private void CreatePlan(IAppEntity app, IAppPlansProvider plans) + public static async Task FromDomainAsync(ITeamEntity team, Resources resources, IUserResolver userResolver, bool invited) { - MaxContributors = plans.GetPlanForApp(app).Plan.MaxContributors; + var users = await userResolver.QueryManyAsync(team.Contributors.Keys.ToArray()); + + var result = new ContributorsDto + { + Items = team.Contributors + .Select(x => ContributorDto.FromDomain(x.Key, x.Value)) + .Select(x => x.CreateUser(users)) + .Select(x => x.CreateTeamLinks(resources)) + .OrderBy(x => x.ContributorName) + .ToArray() + }; + + result.CreateInvited(invited); + + return result.CreateTeamLinks(resources); + } + + private void CreatePlan(Plan plan) + { + MaxContributors = plan.MaxContributors; } private void CreateInvited(bool isInvited) @@ -69,7 +91,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models } } - private ContributorsDto CreateLinks(Resources resources) + private ContributorsDto CreateAppLinks(Resources resources) { var values = new { app = resources.App }; @@ -83,5 +105,20 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models return this; } + + private ContributorsDto CreateTeamLinks(Resources resources) + { + var values = new { team = resources.Team }; + + AddSelfLink(resources.Url(x => nameof(x.GetContributors), values)); + + if (resources.CanAssignTeamContributor) + { + AddPostLink("create", + resources.Url(x => nameof(x.PostContributor), values)); + } + + return this; + } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs b/backend/src/Squidex/Areas/Api/Controllers/ContributorsMetadata.cs similarity index 91% rename from backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs rename to backend/src/Squidex/Areas/Api/Controllers/ContributorsMetadata.cs index 0d0950441..fd9a00ec5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/ContributorsMetadata.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers { public sealed class ContributorsMetadata { diff --git a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index d68a050a6..76ad219c1 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -42,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.History [ProducesResponseType(typeof(HistoryEventDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppHistory)] [ApiCosts(0.1)] - public async Task GetHistory(string app, string channel) + public async Task GetAppHistory(string app, string channel) { var events = await historyService.QueryByChannelAsync(AppId, channel, 100, HttpContext.RequestAborted); @@ -50,5 +50,28 @@ namespace Squidex.Areas.Api.Controllers.History return Ok(response); } + + /// + /// Get historical events for a team. + /// + /// The ID of the team. + /// The name of the channel. + /// + /// 200 => Events returned. + /// 404 => Team not found. + /// + [HttpGet] + [Route("teams/{team}/history/")] + [ProducesResponseType(typeof(HistoryEventDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamHistory)] + [ApiCosts(0.1)] + public async Task GetTeamHistory(string team, string channel) + { + var events = await historyService.QueryByChannelAsync(TeamId, channel, 100, HttpContext.RequestAborted); + + var response = events.Select(HistoryEventDto.FromDomain).Where(x => x.Message != null).ToArray(); + + return Ok(response); + } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index 2e6510122..f284e548a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -8,8 +8,10 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Plans.Models; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; using Squidex.Shared; using Squidex.Web; @@ -21,16 +23,19 @@ namespace Squidex.Areas.Api.Controllers.Plans [ApiExplorerSettings(GroupName = nameof(Plans))] public sealed class AppPlansController : ApiController { - private readonly IAppPlansProvider appPlansProvider; - private readonly IAppPlanBillingManager appPlansBillingManager; + private readonly IBillingPlans billingPlans; + private readonly IBillingManager billingManager; + private readonly IAppUsageGate appUsageGate; public AppPlansController(ICommandBus commandBus, - IAppPlansProvider appPlansProvider, - IAppPlanBillingManager appPlansBillingManager) + IAppUsageGate appUsageGate, + IBillingPlans billingPlans, + IBillingManager billingManager) : base(commandBus) { - this.appPlansProvider = appPlansProvider; - this.appPlansBillingManager = appPlansBillingManager; + this.billingPlans = billingPlans; + this.billingManager = billingManager; + this.appUsageGate = appUsageGate; } /// @@ -43,16 +48,18 @@ namespace Squidex.Areas.Api.Controllers.Plans /// [HttpGet] [Route("apps/{app}/plans/")] - [ProducesResponseType(typeof(AppPlansDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(PlansDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppPlansRead)] [ApiCosts(0)] public IActionResult GetPlans(string app) { - var hasPortal = appPlansBillingManager.HasPortal; + var hasPortal = billingManager.HasPortal; - var response = Deferred.Response(() => + var response = Deferred.AsyncResponse(async () => { - return AppPlansDto.FromDomain(App, appPlansProvider, hasPortal); + var (_, planId, _) = await appUsageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); + + return PlansDto.FromDomain(App, billingPlans, planId, hasPortal); }); Response.Headers[HeaderNames.ETag] = App.ToEtag(); @@ -77,7 +84,9 @@ namespace Squidex.Areas.Api.Controllers.Plans [ApiCosts(0)] public async Task PutPlan(string app, [FromBody] ChangePlanDto request) { - var context = await CommandBus.PublishAsync(request.ToCommand(), HttpContext.RequestAborted); + var command = SimpleMapper.Map(request, new ChangePlan()); + + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); string? redirectUri = null; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs index d0138b65e..d337a8996 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; namespace Squidex.Areas.Api.Controllers.Plans.Models @@ -18,12 +16,5 @@ namespace Squidex.Areas.Api.Controllers.Plans.Models /// [LocalizedRequired] public string PlanId { get; set; } - - public ChangePlan ToCommand() - { - var result = SimpleMapper.Map(this, new ChangePlan()); - - return result; - } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs index 95590d68b..a7fbbdf9b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; @@ -14,7 +14,7 @@ namespace Squidex.Areas.Api.Controllers.Plans.Models public sealed class PlanDto { /// - /// The id of the plan. + /// The ID of the plan. /// [LocalizedRequired] public string Id { get; set; } @@ -47,7 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Plans.Models public string? YearlyCosts { get; set; } /// - /// The yearly id of the plan. + /// The yearly ID of the plan. /// public string? YearlyId { get; set; } @@ -71,7 +71,7 @@ namespace Squidex.Areas.Api.Controllers.Plans.Models /// public int MaxContributors { get; set; } - public static PlanDto FromDomain(IAppLimitsPlan plan) + public static PlanDto FromDomain(Plan plan) { var result = SimpleMapper.Map(plan, new PlanDto()); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlansDto.cs similarity index 60% rename from backend/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlansDto.cs index 626149f20..632fde067 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlansDto.cs @@ -6,12 +6,14 @@ // ========================================================================== using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Infrastructure; using Squidex.Infrastructure.Validation; namespace Squidex.Areas.Api.Controllers.Plans.Models { - public sealed class AppPlansDto + public sealed class PlansDto { /// /// The available plans. @@ -29,20 +31,37 @@ namespace Squidex.Areas.Api.Controllers.Plans.Models /// public string? PlanOwner { get; set; } + /// + /// The ID of the team. + /// + public DomainId? TeamId { get; set; } + /// /// Indicates if there is a billing portal. /// public bool HasPortal { get; set; } - public static AppPlansDto FromDomain(IAppEntity app, IAppPlansProvider plans, bool hasPortal) + public static PlansDto FromDomain(IAppEntity app, IBillingPlans plans, string planId, bool hasPortal) { - var (_, planId) = plans.GetPlanForApp(app); - - var result = new AppPlansDto + var result = new PlansDto { CurrentPlanId = planId, Plans = plans.GetAvailablePlans().Select(PlanDto.FromDomain).ToArray(), PlanOwner = app.Plan?.Owner.Identifier, + HasPortal = hasPortal, + TeamId = app.TeamId + }; + + return result; + } + + public static PlansDto FromDomain(ITeamEntity team, IBillingPlans plans, string planId, bool hasPortal) + { + var result = new PlansDto + { + CurrentPlanId = planId, + Plans = plans.GetAvailablePlans().Select(PlanDto.FromDomain).ToArray(), + PlanOwner = team.Plan?.Owner.Identifier, HasPortal = hasPortal }; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs new file mode 100644 index 000000000..09eeaf047 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs @@ -0,0 +1,100 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Api.Controllers.Plans.Models; +using Squidex.Domain.Apps.Entities.Billing; +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Plans +{ + /// + /// Update and query plans. + /// + [ApiExplorerSettings(GroupName = nameof(Plans))] + public sealed class TeamPlansController : ApiController + { + private readonly IAppUsageGate appUsageGate; + private readonly IBillingPlans billingPlans; + private readonly IBillingManager billingManager; + + public TeamPlansController(ICommandBus commandBus, + IAppUsageGate appUsageGate, + IBillingPlans billingPlans, + IBillingManager billingManager) + : base(commandBus) + { + this.appUsageGate = appUsageGate; + this.billingPlans = billingPlans; + this.billingManager = billingManager; + } + + /// + /// Get team plan information. + /// + /// The name of the team. + /// + /// 200 => Team plan information returned. + /// 404 => Team not found. + /// + [HttpGet] + [Route("teams/{team}/plans/")] + [ProducesResponseType(typeof(PlansDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamPlansRead)] + [ApiCosts(0)] + public IActionResult GetPlans(string team) + { + var hasPortal = billingManager.HasPortal; + + var response = Deferred.AsyncResponse(async () => + { + var (_, planId) = await appUsageGate.GetPlanForTeamAsync(Team, HttpContext.RequestAborted); + + return PlansDto.FromDomain(Team, billingPlans, planId, hasPortal); + }); + + Response.Headers[HeaderNames.ETag] = Team.ToEtag(); + + return Ok(response); + } + + /// + /// Change the team plan. + /// + /// The name of the team. + /// Plan object that needs to be changed. + /// + /// 200 => Plan changed or redirect url returned. + /// 404 => Team not found. + /// + [HttpPut] + [Route("teams/{team}/plan/")] + [ProducesResponseType(typeof(PlanChangedDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamPlansChange)] + [ApiCosts(0)] + public async Task PutPlan(string team, [FromBody] ChangePlanDto request) + { + var command = SimpleMapper.Map(request, new ChangePlan()); + + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + + string? redirectUri = null; + + if (context.PlainResult is PlanChangedResult result) + { + redirectUri = result.RedirectUri?.ToString(); + } + + return Ok(new PlanChangedDto { RedirectUri = redirectUri }); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs index bf251ba7c..642e24c77 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs @@ -21,7 +21,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models public sealed class RuleDto : Resource { /// - /// The id of the rule. + /// The ID of the rule. /// public DomainId Id { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs index d0bab6bf3..aa5500716 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs @@ -18,7 +18,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models public sealed class RuleEventDto : Resource { /// - /// The id of the event. + /// The ID of the event. /// public DomainId Id { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs index 37137d605..76e025f16 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs @@ -22,7 +22,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models public RuleDto[] Items { get; set; } /// - /// The id of the rule that is currently rerunning. + /// The ID of the rule that is currently rerunning. /// public DomainId? RunningRuleId { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs index e05b642f5..0e8e2ccac 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs @@ -14,7 +14,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers public sealed class ContentChangedRuleTriggerSchemaDto { /// - /// The id of the schema. + /// The ID of the schema. /// public DomainId SchemaId { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index 488b6f153..b5eb8fb2d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -153,7 +153,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// Update a rule. /// /// The name of the app. - /// The id of the rule to update. + /// The ID of the rule to update. /// The rule object that needs to be added to the app. /// /// 200 => Rule updated. @@ -178,7 +178,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// Enable a rule. /// /// The name of the app. - /// The id of the rule to enable. + /// The ID of the rule to enable. /// /// 200 => Rule enabled. /// 404 => Rule or app not found. @@ -201,7 +201,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// Disable a rule. /// /// The name of the app. - /// The id of the rule to disable. + /// The ID of the rule to disable. /// /// 200 => Rule disabled. /// 404 => Rule or app not found. @@ -224,7 +224,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// Trigger a rule. /// /// The name of the app. - /// The id of the rule to disable. + /// The ID of the rule to disable. /// /// 204 => Rule triggered. /// 404 => Rule or app not found. @@ -246,7 +246,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// Run a rule. /// /// The name of the app. - /// The id of the rule to run. + /// The ID of the rule to run. /// Runs the rule from snapeshots if possible. /// /// 204 => Rule started. @@ -267,7 +267,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// Cancels all rule events. /// /// The name of the app. - /// The id of the rule to cancel. + /// The ID of the rule to cancel. /// /// 204 => Rule events cancelled. /// @@ -312,7 +312,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// Simulate a rule. /// /// The name of the app. - /// The id of the rule to simulate. + /// The ID of the rule to simulate. /// /// 200 => Rule simulated. /// 404 => Rule or app not found. @@ -342,7 +342,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// Delete a rule. /// /// The name of the app. - /// The id of the rule to delete. + /// The ID of the rule to delete. /// /// 204 => Rule deleted. /// 404 => Rule or app not found. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs index 891d06fe0..8b4f933da 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs @@ -16,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models public sealed class FieldDto : Resource { /// - /// The id of the field. + /// The ID of the field. /// public long FieldId { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentFieldPropertiesDto.cs index 956832e98..78d383472 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentFieldPropertiesDto.cs @@ -15,7 +15,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields public sealed class ComponentFieldPropertiesDto : FieldPropertiesDto { /// - /// The id of the embedded schemas. + /// The ID of the embedded schemas. /// public ReadonlyList? SchemaIds { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs index dfcbcc410..88ea34771 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs @@ -25,7 +25,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields public int? MaxItems { get; set; } /// - /// The id of the embedded schemas. + /// The ID of the embedded schemas. /// public ReadonlyList? SchemaIds { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs index ea56d0c74..43c8bffff 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs @@ -55,7 +55,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields public ReferencesFieldEditor Editor { get; set; } /// - /// The id of the referenced schemas. + /// The ID of the referenced schemas. /// public ReadonlyList? SchemaIds { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs index bf9550db6..7b62d2a5b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs @@ -13,7 +13,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models public sealed class NestedFieldDto : Resource { /// - /// The id of the field. + /// The ID of the field. /// public long FieldId { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs index 670b8527e..59a44d0ce 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs @@ -20,7 +20,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models public class SchemaDto : Resource { /// - /// The id of the schema. + /// The ID of the schema. /// public DomainId Id { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs index 1377e0a49..c7a31761c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs @@ -59,7 +59,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// public bool IsPublished { get; set; } - public static T ToCommand(TSoure dto, T command) where T : SchemaCommand, IUpsertCommand where TSoure : UpsertSchemaDto + public static T ToCommand(TSoure dto, T command) where T : SchemaCommandBase, IUpsertCommand where TSoure : UpsertSchemaDto { SimpleMapper.Map(dto, command); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index c613f1562..cb5b60416 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -160,7 +160,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// /// The name of the app. /// The name of the schema. - /// The id of the field to update. + /// The ID of the field to update. /// The field object that needs to be added to the schema. /// /// 200 => Schema field updated. @@ -187,7 +187,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The name of the app. /// The name of the schema. /// The parent field id. - /// The id of the field to update. + /// The ID of the field to update. /// The field object that needs to be added to the schema. /// /// 200 => Schema field updated. @@ -213,7 +213,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// /// The name of the app. /// The name of the schema. - /// The id of the field to lock. + /// The ID of the field to lock. /// /// 200 => Schema field shown. /// 400 => Schema field request not valid or field locked. @@ -242,7 +242,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The name of the app. /// The name of the schema. /// The parent field id. - /// The id of the field to lock. + /// The ID of the field to lock. /// /// 200 => Schema field hidden. /// 400 => Schema field request not valid or field locked. @@ -270,7 +270,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// /// The name of the app. /// The name of the schema. - /// The id of the field to hide. + /// The ID of the field to hide. /// /// 200 => Schema field hidden. /// 400 => Schema field request not valid or field locked. @@ -299,7 +299,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The name of the app. /// The name of the schema. /// The parent field id. - /// The id of the field to hide. + /// The ID of the field to hide. /// /// 200 => Schema field hidden. /// 400 => Schema field request not valid or field locked. @@ -327,7 +327,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// /// The name of the app. /// The name of the schema. - /// The id of the field to show. + /// The ID of the field to show. /// /// 200 => Schema field shown. /// 400 => Schema field request not valid or field locked. @@ -356,7 +356,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The name of the app. /// The name of the schema. /// The parent field id. - /// The id of the field to show. + /// The ID of the field to show. /// /// 200 => Schema field shown. /// 400 => Schema field request not valid or field locked. @@ -384,7 +384,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// /// The name of the app. /// The name of the schema. - /// The id of the field to enable. + /// The ID of the field to enable. /// /// 200 => Schema field enabled. /// 400 => Schema field request not valid or field locked. @@ -413,7 +413,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The name of the app. /// The name of the schema. /// The parent field id. - /// The id of the field to enable. + /// The ID of the field to enable. /// /// 200 => Schema field enabled. /// 400 => Schema field request not valid or field locked. @@ -441,7 +441,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// /// The name of the app. /// The name of the schema. - /// The id of the field to disable. + /// The ID of the field to disable. /// /// 200 => Schema field disabled. /// 400 => Schema field request not valid or field locked. @@ -470,7 +470,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The name of the app. /// The name of the schema. /// The parent field id. - /// The id of the field to disable. + /// The ID of the field to disable. /// /// 200 => Schema field disabled. /// 400 => Schema field request not valid or field locked. @@ -498,7 +498,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// /// The name of the app. /// The name of the schema. - /// The id of the field to disable. + /// The ID of the field to disable. /// /// 200 => Schema field deleted. /// 400 => Schema field request not valid or field locked. @@ -524,7 +524,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The name of the app. /// The name of the schema. /// The parent field id. - /// The id of the field to disable. + /// The ID of the field to disable. /// /// 200 => Schema field deleted. /// 400 => Schema field request not valid or field locked. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs index 97db80196..d616a903a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.Validation; @@ -59,7 +59,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics.Models [LocalizedRequired] public Dictionary Details { get; set; } - public static CallsUsageDtoDto FromDomain(IAppLimitsPlan plan, ApiStatsSummary summary, Dictionary> details) + public static CallsUsageDtoDto FromDomain(Plan plan, ApiStatsSummary summary, Dictionary> details) { return new CallsUsageDtoDto { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index 148a83115..410961fe4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -9,8 +9,8 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Statistics.Models; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Hosting; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -21,14 +21,14 @@ using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Statistics { /// - /// Retrieves usage information for apps. + /// Retrieves usage information for apps and teams. /// [ApiExplorerSettings(GroupName = nameof(Statistics))] public sealed class UsagesController : ApiController { private readonly IApiUsageTracker usageTracker; private readonly IAppLogStore appLogStore; - private readonly IAppPlansProvider appPlansProvider; + private readonly IAppUsageGate appUsageGate; private readonly IAssetUsageTracker assetStatsRepository; private readonly IDataProtector dataProtector; private readonly IUrlGenerator urlGenerator; @@ -38,7 +38,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics IDataProtectionProvider dataProtection, IApiUsageTracker usageTracker, IAppLogStore appLogStore, - IAppPlansProvider appPlansProvider, + IAppUsageGate appUsageGate, IAssetUsageTracker assetStatsRepository, IUrlGenerator urlGenerator) : base(commandBus) @@ -46,7 +46,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics this.usageTracker = usageTracker; this.appLogStore = appLogStore; - this.appPlansProvider = appPlansProvider; + this.appUsageGate = appUsageGate; this.assetStatsRepository = assetStatsRepository; this.urlGenerator = urlGenerator; @@ -78,6 +78,28 @@ namespace Squidex.Areas.Api.Controllers.Statistics return Ok(response); } + [HttpGet] + [Route("apps/log/{token}/")] + [ApiExplorerSettings(IgnoreApi = true)] + public IActionResult GetLogFile(string token) + { + // Decrypt the token that has previously been generated. + var appId = DomainId.Create(dataProtector.Unprotect(token)); + + var fileDate = DateTime.UtcNow.Date; + var fileName = $"Usage-{fileDate:yyy-MM-dd}.csv"; + + var callback = new FileCallback((body, range, ct) => + { + return appLogStore.ReadLogAsync(appId, fileDate.AddDays(-30), fileDate, body, ct); + }); + + return new FileCallbackResult("text/csv", callback) + { + FileDownloadName = fileName + }; + } + /// /// Get api calls in date range. /// @@ -105,7 +127,41 @@ namespace Squidex.Areas.Api.Controllers.Statistics var (summary, details) = await usageTracker.QueryAsync(AppId.ToString(), fromDate.Date, toDate.Date, HttpContext.RequestAborted); // Use the current app plan to show the limits to the user. - var (plan, _) = appPlansProvider.GetPlanForApp(App); + var (plan, _, _) = await appUsageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); + + var response = CallsUsageDtoDto.FromDomain(plan, summary, details); + + return Ok(response); + } + + /// + /// Get api calls in date range for team. + /// + /// The name of the team. + /// The from date. + /// The to date. + /// + /// 200 => API call returned. + /// 400 => Range between from date and to date is not valid or has more than 100 days. + /// 404 => Team not found. + /// + [HttpGet] + [Route("teams/{team}/usages/calls/{fromDate}/{toDate}/")] + [ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamUsage)] + [ApiCosts(0)] + public async Task GetUsagesForTeam(string team, DateTime fromDate, DateTime toDate) + { + // We can only query 100 logs for up to 100 days. + if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) + { + return BadRequest(); + } + + var (summary, details) = await usageTracker.QueryAsync(TeamId.ToString(), fromDate.Date, toDate.Date, HttpContext.RequestAborted); + + // Use the current team plan to show the limits to the user. + var (plan, _) = await appUsageGate.GetPlanForTeamAsync(Team, HttpContext.RequestAborted); var response = CallsUsageDtoDto.FromDomain(plan, summary, details); @@ -127,10 +183,35 @@ namespace Squidex.Areas.Api.Controllers.Statistics [ApiCosts(0)] public async Task GetCurrentStorageSize(string app) { - var size = await assetStatsRepository.GetTotalSizeAsync(AppId); + var size = await assetStatsRepository.GetTotalSizeByAppAsync(AppId, HttpContext.RequestAborted); // Use the current app plan to show the limits to the user. - var (plan, _) = appPlansProvider.GetPlanForApp(App); + var (plan, _, _) = await appUsageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); + + var response = new CurrentStorageDto { Size = size, MaxAllowed = plan.MaxAssetSize }; + + return Ok(response); + } + + /// + /// Get total asset size by team. + /// + /// The ID of the team. + /// + /// 200 => Storage usage returned. + /// 404 => Team not found. + /// + [HttpGet] + [Route("teams/{team}/usages/storage/today/")] + [ProducesResponseType(typeof(CurrentStorageDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamUsage)] + [ApiCosts(0)] + public async Task GetTeamCurrentStorageSizeForTeam(string team) + { + var size = await assetStatsRepository.GetTotalSizeByTeamAsync(TeamId, HttpContext.RequestAborted); + + // Use the current team plan to show the limits to the user. + var (plan, _) = await appUsageGate.GetPlanForTeamAsync(Team, HttpContext.RequestAborted); var response = new CurrentStorageDto { Size = size, MaxAllowed = plan.MaxAssetSize }; @@ -160,33 +241,41 @@ namespace Squidex.Areas.Api.Controllers.Statistics return BadRequest(); } - var usages = await assetStatsRepository.QueryAsync(AppId, fromDate.Date, toDate.Date); + var usages = await assetStatsRepository.QueryByAppAsync(AppId, fromDate.Date, toDate.Date, HttpContext.RequestAborted); var models = usages.Select(StorageUsagePerDateDto.FromDomain).ToArray(); return Ok(models); } + /// + /// Get asset usage by date for team. + /// + /// The ID of the team. + /// The from date. + /// The to date. + /// + /// 200 => Storage usage returned. + /// 400 => Range between from date and to date is not valid or has more than 100 days. + /// 404 => Team not found. + /// [HttpGet] - [Route("apps/log/{token}/")] - [ApiExplorerSettings(IgnoreApi = true)] - public IActionResult GetLogFile(string token) + [Route("teams/{team}/usages/storage/{fromDate}/{toDate}/")] + [ProducesResponseType(typeof(StorageUsagePerDateDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamUsage)] + [ApiCosts(0)] + public async Task GetStorageSizesForTeam(string team, DateTime fromDate, DateTime toDate) { - // Decrypt the token that has previously been generated. - var appId = DomainId.Create(dataProtector.Unprotect(token)); + if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) + { + return BadRequest(); + } - var fileDate = DateTime.UtcNow.Date; - var fileName = $"Usage-{fileDate:yyy-MM-dd}.csv"; + var usages = await assetStatsRepository.QueryByTeamAsync(TeamId, fromDate.Date, toDate.Date, HttpContext.RequestAborted); - var callback = new FileCallback((body, range, ct) => - { - return appLogStore.ReadLogAsync(appId, fileDate.AddDays(-30), fileDate, body, ct); - }); + var models = usages.Select(StorageUsagePerDateDto.FromDomain).ToArray(); - return new FileCallbackResult("text/csv", callback) - { - FileDownloadName = fileName - }; + return Ok(models); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/CreateTeamDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/CreateTeamDto.cs new file mode 100644 index 000000000..ea11fa2b0 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/CreateTeamDto.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Areas.Api.Controllers.Teams.Models +{ + public sealed class CreateTeamDto + { + /// + /// The name of the team. + /// + [LocalizedRequired] + public string Name { get; set; } + + public CreateTeam ToCommand() + { + return SimpleMapper.Map(this, new CreateTeam()); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/TeamDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/TeamDto.cs new file mode 100644 index 000000000..fce9c648e --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/TeamDto.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Areas.Api.Controllers.Plans; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Validation; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Teams.Models +{ + public sealed class TeamDto : Resource + { + /// + /// The ID of the team. + /// + public DomainId Id { get; set; } + + /// + /// The name of the team. + /// + [LocalizedRequired] + public string Name { get; set; } + + /// + /// The version of the team. + /// + public long Version { get; set; } + + /// + /// The timestamp when the team has been created. + /// + public Instant Created { get; set; } + + /// + /// The timestamp when the team has been modified last. + /// + public Instant LastModified { get; set; } + + /// + /// The role name of the user. + /// + public string? RoleName { get; set; } + + public static TeamDto FromDomain(ITeamEntity team, string userId, Resources resources) + { + var result = SimpleMapper.Map(team, new TeamDto()); + + var permissions = PermissionSet.Empty; + + if (team.TryGetContributorRole(userId, out _)) + { + permissions = new PermissionSet(PermissionIds.ForApp(PermissionIds.TeamAdmin, team: team.Id.ToString())); + } + + return result.CreateLinks(team, resources, permissions, true); + } + + private TeamDto CreateLinks(ITeamEntity team, Resources resources, PermissionSet permissions, bool isContributor) + { + var values = new { team = Id.ToString() }; + + if (isContributor) + { + AddDeleteLink("leave", + resources.Url(x => nameof(x.DeleteMyself), values)); + } + + if (resources.IsAllowed(PermissionIds.TeamUpdate, team: values.team, additional: permissions)) + { + AddPutLink("update", + resources.Url(x => nameof(x.PutTeam), values)); + } + + if (resources.IsAllowed(PermissionIds.TeamContributorsRead, team: values.team, additional: permissions)) + { + AddGetLink("contributors", + resources.Url(x => nameof(x.GetContributors), values)); + } + + if (resources.IsAllowed(PermissionIds.TeamPlansRead, team: values.team, additional: permissions)) + { + AddGetLink("plans", + resources.Url(x => nameof(x.GetPlans), values)); + } + + return this; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/UpdateTeamDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/UpdateTeamDto.cs new file mode 100644 index 000000000..2c70e2409 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/UpdateTeamDto.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Areas.Api.Controllers.Teams.Models +{ + public sealed class UpdateTeamDto + { + /// + /// The name of the team. + /// + [LocalizedRequired] + public string Name { get; set; } + + public UpdateTeam ToCommand() + { + return SimpleMapper.Map(this, new UpdateTeam()); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs new file mode 100644 index 000000000..0f064dd46 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs @@ -0,0 +1,148 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Squidex.Domain.Apps.Entities.Invitation; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared; +using Squidex.Shared.Users; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Teams +{ + /// + /// Update and query teams. + /// + [ApiExplorerSettings(GroupName = nameof(Teams))] + public sealed class TeamContributorsController : ApiController + { + private readonly IUserResolver userResolver; + + public TeamContributorsController(ICommandBus commandBus, IUserResolver userResolver) + : base(commandBus) + { + this.userResolver = userResolver; + } + + /// + /// Get team contributors. + /// + /// The ID of the team. + /// + /// 200 => Contributors returned. + /// 404 => Team not found. + /// + [HttpGet] + [Route("teams/{team}/contributors/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamContributorsRead)] + [ApiCosts(0)] + public IActionResult GetContributors(string team) + { + var response = Deferred.AsyncResponse(() => + { + return GetResponseAsync(Team, false); + }); + + Response.Headers[HeaderNames.ETag] = Team.ToEtag(); + + return Ok(response); + } + + /// + /// Assign contributor to team. + /// + /// The ID of the team. + /// Contributor object that needs to be added to the team. + /// + /// 201 => Contributor assigned to team. + /// 400 => Contributor request not valid. + /// 404 => Team not found. + /// + [HttpPost] + [Route("teams/{team}/contributors/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status201Created)] + [ApiPermissionOrAnonymous(PermissionIds.TeamContributorsAssign)] + [ApiCosts(1)] + public async Task PostContributor(string team, [FromBody] AssignContributorDto request) + { + var command = SimpleMapper.Map(request, new AssignContributor()); + + var response = await InvokeCommandAsync(command); + + return CreatedAtAction(nameof(GetContributors), new { team }, response); + } + + /// + /// Remove yourself. + /// + /// The ID of the team. + /// + /// 200 => Contributor removed. + /// 404 => Contributor or team not found. + /// + [HttpDelete] + [Route("teams/{team}/contributors/me/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(1)] + public async Task DeleteMyself(string team) + { + var command = new RemoveContributor { ContributorId = UserId }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// + /// Remove contributor. + /// + /// The ID of the team. + /// The ID of the contributor. + /// + /// 200 => Contributor removed. + /// 404 => Contributor or team not found. + /// + [HttpDelete] + [Route("teams/{team}/contributors/{id}/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamContributorsRevoke)] + [ApiCosts(1)] + public async Task DeleteContributor(string team, string id) + { + var command = new RemoveContributor { ContributorId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + private async Task InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + + if (context.PlainResult is InvitedResult invited) + { + return await GetResponseAsync(invited.Entity, true); + } + else + { + return await GetResponseAsync(context.Result(), false); + } + } + + private async Task GetResponseAsync(ITeamEntity team, bool invited) + { + return await ContributorsDto.FromDomainAsync(team, Resources, userResolver, invited); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs new file mode 100644 index 000000000..ffbeeca08 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs @@ -0,0 +1,152 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Api.Controllers.Teams.Models; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Infrastructure.Commands; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Teams +{ + /// + /// Update and query teams. + /// + [ApiExplorerSettings(GroupName = nameof(Teams))] + public sealed class TeamsController : ApiController + { + private readonly IAppProvider appProvider; + + public TeamsController(ICommandBus commandBus, IAppProvider appProvider) + : base(commandBus) + { + this.appProvider = appProvider; + } + + /// + /// Get your teams. + /// + /// + /// 200 => Teams returned. + /// + /// + /// You can only retrieve the list of teams when you are authenticated as a user (OpenID implicit flow). + /// You will retrieve all teams, where you are assigned as a contributor. + /// + [HttpGet] + [Route("teams/")] + [ProducesResponseType(typeof(TeamDto[]), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(0)] + public async Task GetTeams() + { + var teams = await appProvider.GetUserTeamsAsync(UserOrClientId, HttpContext.RequestAborted); + + var response = Deferred.Response(() => + { + return teams.OrderBy(x => x.Name).Select(a => TeamDto.FromDomain(a, UserOrClientId, Resources)).ToArray(); + }); + + Response.Headers[HeaderNames.ETag] = teams.ToEtag(); + + return Ok(response); + } + + /// + /// Get an team by name. + /// + /// The name of the team. + /// + /// 200 => Teams returned. + /// 404 => Team not found. + /// + [HttpGet] + [Route("teams/{team}")] + [ProducesResponseType(typeof(TeamDto), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(0)] + public IActionResult GetTeam(string team) + { + var response = Deferred.Response(() => + { + return TeamDto.FromDomain(Team, UserOrClientId, Resources); + }); + + Response.Headers[HeaderNames.ETag] = Team.ToEtag(); + + return Ok(response); + } + + /// + /// Create a new team. + /// + /// The team object that needs to be added to Squidex. + /// + /// 201 => Team created. + /// 400 => Team request not valid. + /// 409 => Team name is already in use. + /// + /// + /// You can only create an team when you are authenticated as a user (OpenID implicit flow). + /// You will be assigned as owner of the new team automatically. + /// + [HttpPost] + [Route("teams/")] + [ProducesResponseType(typeof(TeamDto), 201)] + [ApiPermission] + [ApiCosts(0)] + public async Task PostTeam([FromBody] CreateTeamDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); + + return CreatedAtAction(nameof(GetTeams), response); + } + + /// + /// Update the team. + /// + /// The name of the team to update. + /// The values to update. + /// + /// 200 => Team updated. + /// 400 => Team request not valid. + /// 404 => Team not found. + /// + [HttpPut] + [Route("teams/{team}/")] + [ProducesResponseType(typeof(TeamDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamUpdate)] + [ApiCosts(0)] + public async Task PutTeam(string team, [FromBody] UpdateTeamDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); + + return Ok(response); + } + + private Task InvokeCommandAsync(ICommand command) + { + return InvokeCommandAsync(command, x => + { + return TeamDto.FromDomain(x, UserOrClientId, Resources); + }); + } + + private async Task InvokeCommandAsync(ICommand command, Func converter) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + + var result = context.Result(); + var response = converter(result); + + return response; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs index e17bfeaa3..8ff567a10 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs @@ -13,5 +13,10 @@ namespace Squidex.Areas.Api.Controllers.UI.Models /// True when the user can create apps. /// public bool CanCreateApps { get; set; } + + /// + /// True when the user can create teams. + /// + public bool CanCreateTeams { get; set; } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs index 07ff74b34..dd60f36c4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs @@ -47,6 +47,9 @@ namespace Squidex.Areas.Api.Controllers.UI [JsonPropertyName("onlyAdminsCanCreateApps")] public bool OnlyAdminsCanCreateApps { get; set; } + [JsonPropertyName("onlyAdminsCanCreateTeams")] + public bool OnlyAdminsCanCreateTeams { get; set; } + public sealed class MapOptions { [JsonPropertyName("type")] diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index 1989cf9f8..6cce1f255 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs @@ -9,10 +9,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Squidex.Areas.Api.Controllers.UI.Models; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Translations; using Squidex.Shared; using Squidex.Web; @@ -21,6 +19,7 @@ namespace Squidex.Areas.Api.Controllers.UI public sealed class UIController : ApiController { private static readonly Permission CreateAppPermission = new Permission(PermissionIds.AdminAppCreate); + private static readonly Permission CreateTeamPermission = new Permission(PermissionIds.AdminTeamCreate); private readonly MyUIOptions uiOptions; private readonly IAppUISettings appUISettings; @@ -46,7 +45,8 @@ namespace Squidex.Areas.Api.Controllers.UI { var result = new UISettingsDto { - CanCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.UserPermissions.Includes(CreateAppPermission) + CanCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.UserPermissions.Includes(CreateAppPermission), + CanCreateTeams = !uiOptions.OnlyAdminsCanCreateApps || Context.UserPermissions.Includes(CreateTeamPermission), }; return Ok(result); @@ -85,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task GetUserSettings(string app) { - var result = await appUISettings.GetAsync(AppId, UserId(), HttpContext.RequestAborted); + var result = await appUISettings.GetAsync(AppId, UserId, HttpContext.RequestAborted); return Ok(result); } @@ -125,7 +125,7 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task PutUserSetting(string app, string key, [FromBody] UpdateSettingDto request) { - await appUISettings.SetAsync(AppId, UserId(), key, request.Value, HttpContext.RequestAborted); + await appUISettings.SetAsync(AppId, UserId, key, request.Value, HttpContext.RequestAborted); return NoContent(); } @@ -163,21 +163,9 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task DeleteUserSetting(string app, string key) { - await appUISettings.RemoveAsync(AppId, UserId(), key, HttpContext.RequestAborted); + await appUISettings.RemoveAsync(AppId, UserId, key, HttpContext.RequestAborted); return NoContent(); } - - private string UserId() - { - var subject = User.OpenIdSubject(); - - if (string.IsNullOrWhiteSpace(subject)) - { - throw new DomainForbiddenException(T.Get("common.httpOnlyAsUser")); - } - - return subject; - } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs index 4d285bd70..34f602025 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs @@ -16,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.Users.Models public sealed class UserDto : Resource { /// - /// The id of the user. + /// The ID of the user. /// [LocalizedRequired] public string Id { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs index 48842195a..2478bb6e5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -107,7 +107,7 @@ namespace Squidex.Areas.Api.Controllers.Users /// /// Get user by id. /// - /// The id of the user (GUID). + /// The ID of the user (GUID). /// /// 200 => User found. /// 404 => User not found. @@ -140,7 +140,7 @@ namespace Squidex.Areas.Api.Controllers.Users /// /// Get user picture by id. /// - /// The id of the user (GUID). + /// The ID of the user (GUID). /// /// 200 => User found and image or fallback returned. /// 404 => User not found. diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs b/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs index f679f960c..3ae1b57ff 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs @@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Entities; using Squidex.Domain.Users; using Squidex.Domain.Users.InMemory; using Squidex.Hosting; -using Squidex.Infrastructure; using Squidex.Shared.Identity; using Squidex.Shared.Users; using Squidex.Web; diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs index e51401e14..c823fddff 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs @@ -103,6 +103,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Setup BaseUrlCurrent = GetCurrentUrl(), ErrorMessage = errorMessage, EverybodyCanCreateApps = !uiOptions.OnlyAdminsCanCreateApps, + EverybodyCanCreateTeams = !uiOptions.OnlyAdminsCanCreateTeams, IsValidHttps = HttpContext.Request.IsHttps, IsAssetStoreFile = assetStore is FolderAssetStore, IsAssetStoreFtp = assetStore is FTPAssetStore, diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs index 228db32cb..0ec56fe39 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs @@ -25,6 +25,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Setup public bool EverybodyCanCreateApps { get; set; } + public bool EverybodyCanCreateTeams { get; set; } + public bool HasExternalLogin { get; set; } public bool HasPasswordAuth { get; set; } diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml index 76725468f..f037db1a1 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml @@ -103,6 +103,15 @@ RenderRuleAsWarning(T.Get("setup.ruleAppCreation.warningAll")); } + @if (Model!.EverybodyCanCreateTeams) + { + RenderRuleAsWarning(T.Get("setup.ruleTeamCreation.warningAdmins")); + } + else + { + RenderRuleAsWarning(T.Get("setup.ruleTeamCreation.warningAll")); + } + @if (Model!.IsAssetStoreFtp) { RenderRuleAsWarning(T.Get("setup.ruleFtp.warning")); diff --git a/backend/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs b/backend/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs index 2b7ce38a6..e81a9bd8c 100644 --- a/backend/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs +++ b/backend/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs @@ -6,17 +6,17 @@ // ========================================================================== using System.Security.Claims; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; namespace Squidex.Areas.Portal.Middlewares { public sealed class PortalRedirectMiddleware { - private readonly IAppPlanBillingManager appPlansBillingManager; + private readonly IBillingManager billingManager; - public PortalRedirectMiddleware(RequestDelegate next, IAppPlanBillingManager appPlansBillingManager) + public PortalRedirectMiddleware(RequestDelegate next, IBillingManager billingManager) { - this.appPlansBillingManager = appPlansBillingManager; + this.billingManager = billingManager; } public async Task Invoke(HttpContext context) @@ -27,7 +27,7 @@ namespace Squidex.Areas.Portal.Middlewares if (userIdClaim != null) { - var portalLink = await appPlansBillingManager.GetPortalLinkAsync(userIdClaim.Value); + var portalLink = await billingManager.GetPortalLinkAsync(userIdClaim.Value, context.RequestAborted); context.Response.Redirect(portalLink); } diff --git a/backend/src/Squidex/Config/Authentication/OidcHandler.cs b/backend/src/Squidex/Config/Authentication/OidcHandler.cs index af835fe78..64d95bf0c 100644 --- a/backend/src/Squidex/Config/Authentication/OidcHandler.cs +++ b/backend/src/Squidex/Config/Authentication/OidcHandler.cs @@ -28,7 +28,8 @@ namespace Squidex.Config.Authentication { var permissions = options.OidcRoleMapping .Where(r => identity.HasClaim(options.OidcRoleClaimType, r.Key)) - .SelectMany(r => r.Value) + .Select(r => r.Value) + .SelectMany(r => r) .Distinct(); foreach (var permission in permissions) diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index 29ac5b763..f8afda87b 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -75,7 +75,7 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As().As().As(); + .As().As(); services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index 4d12531b7..43b02b001 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -8,7 +8,6 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Domain.Apps.Entities.Apps.Invitation; using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets.Commands; @@ -16,12 +15,16 @@ using Squidex.Domain.Apps.Entities.Assets.DomainObject; using Squidex.Domain.Apps.Entities.Comments.DomainObject; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.DomainObject; +using Squidex.Domain.Apps.Entities.Invitation; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Indexes; using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.DomainObject; using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Domain.Apps.Entities.Teams.DomainObject; +using Squidex.Domain.Apps.Entities.Teams.Indexes; using Squidex.Infrastructure.Commands; using Squidex.Web.CommandMiddlewares; @@ -58,6 +61,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); @@ -88,6 +94,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As().As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); @@ -97,6 +106,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); @@ -106,13 +118,13 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() + services.AddSingletonAs>() .As(); - services.AddSingletonAs>() + services.AddSingletonAs>() .As(); - services.AddSingletonAs>() + services.AddSingletonAs>() .As(); services.AddSingletonAs() diff --git a/backend/src/Squidex/Config/Domain/NotificationsServices.cs b/backend/src/Squidex/Config/Domain/NotificationsServices.cs index 68b65736a..0cb9e2279 100644 --- a/backend/src/Squidex/Config/Domain/NotificationsServices.cs +++ b/backend/src/Squidex/Config/Domain/NotificationsServices.cs @@ -6,7 +6,7 @@ // ========================================================================== using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Entities.Apps.Invitation; +using Squidex.Domain.Apps.Entities.Invitation; using Squidex.Domain.Apps.Entities.Notifications; using Squidex.Infrastructure.Email; using Squidex.Infrastructure.EventSourcing; diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index 6794af5e0..963f3972f 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -29,12 +29,15 @@ using Squidex.Domain.Apps.Entities.MongoDb.Contents; using Squidex.Domain.Apps.Entities.MongoDb.History; using Squidex.Domain.Apps.Entities.MongoDb.Rules; using Squidex.Domain.Apps.Entities.MongoDb.Schemas; +using Squidex.Domain.Apps.Entities.MongoDb.Teams; using Squidex.Domain.Apps.Entities.MongoDb.Text; using Squidex.Domain.Apps.Entities.Rules.DomainObject; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.DomainObject; using Squidex.Domain.Apps.Entities.Schemas.Repositories; +using Squidex.Domain.Apps.Entities.Teams.DomainObject; +using Squidex.Domain.Apps.Entities.Teams.Repositories; using Squidex.Domain.Users; using Squidex.Domain.Users.InMemory; using Squidex.Domain.Users.MongoDb; @@ -137,6 +140,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As().As>().As(); + services.AddSingletonAs() + .As().As>(); + services.AddSingletonAs() .As().As>().As(); diff --git a/backend/src/Squidex/Config/Domain/SubscriptionServices.cs b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs index c1c7b4e51..d65bb4167 100644 --- a/backend/src/Squidex/Config/Domain/SubscriptionServices.cs +++ b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs @@ -6,7 +6,8 @@ // ========================================================================== using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure; using Squidex.Web; @@ -18,14 +19,14 @@ namespace Squidex.Config.Domain { services.AddSingletonAs(c => c.GetRequiredService>()?.Value?.Plans.OrEmpty()!); - services.AddSingletonAs() - .AsOptional(); + services.AddSingletonAs() + .AsOptional(); - services.AddSingletonAs() - .AsOptional(); + services.AddSingletonAs() + .AsOptional(); services.AddSingletonAs() - .AsSelf(); + .AsOptional().As(); } } } diff --git a/backend/src/Squidex/Config/Domain/TeamServices.cs b/backend/src/Squidex/Config/Domain/TeamServices.cs new file mode 100644 index 000000000..3f30c0339 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/TeamServices.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Teams.Entities.Teams; + +namespace Squidex.Config.Domain +{ + public static class TeamServices + { + public static void AddSquidexTeams(this IServiceCollection services) + { + services.AddSingletonAs() + .As(); + } + } +} diff --git a/backend/src/Squidex/Config/Messaging/MessagingServices.cs b/backend/src/Squidex/Config/Messaging/MessagingServices.cs index 07299bc36..80334d39c 100644 --- a/backend/src/Squidex/Config/Messaging/MessagingServices.cs +++ b/backend/src/Squidex/Config/Messaging/MessagingServices.cs @@ -7,9 +7,9 @@ using System.Text.Json; using Squidex.Domain.Apps.Core.Subscriptions; -using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Runner; diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index 4c1d9d375..49ba99139 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -46,6 +46,9 @@ namespace Squidex.Config.Web services.AddSingletonAs() .AsSelf(); + services.AddSingletonAs() + .AsSelf(); + services.AddSingletonAs() .AsSelf(); @@ -77,7 +80,9 @@ namespace Squidex.Config.Web // Never change this order here. options.Filters.Add(); options.Filters.Add(); + options.Filters.Add(); options.Filters.Add(); + options.Filters.Add(); options.Filters.Add(); options.Filters.Add(); diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs index d71b8eed0..adc320dc8 100644 --- a/backend/src/Squidex/Startup.cs +++ b/backend/src/Squidex/Startup.cs @@ -69,6 +69,7 @@ namespace Squidex services.AddSquidexSerializers(); services.AddSquidexStoreServices(config); services.AddSquidexSubscriptions(config); + services.AddSquidexTeams(); services.AddSquidexTelemetry(config); services.AddSquidexTranslation(config); services.AddSquidexUsageTracking(config); diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 5641ba469..bf11fba88 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -198,7 +198,7 @@ // Configure notifo if you want to have support for custom notifications. "notifo": { - // The id of the app in notifo. + // The ID of the app in notifo. "appId": "", // The API key for your app in notifo. "apiKey": "", diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsJsonTests.cs index e2e7ad17e..8044b309d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsJsonTests.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_serialize_and_deserialize() { - var contributors = AppContributors.Empty; + var contributors = Contributors.Empty; contributors = contributors.Assign("1", Role.Developer); contributors = contributors.Assign("2", Role.Editor); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs index 8892a2246..c3e2ba209 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { public class AppContributorsTests { - private readonly AppContributors contributors_0 = AppContributors.Empty; + private readonly Contributors contributors_0 = Contributors.Empty; [Fact] public void Should_assign_new_contributor() diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs index 4c3fe4065..e9ef1b387 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Infrastructure; using Xunit; @@ -17,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_serialize_and_deserialize() { - var plan = new AppPlan(RefToken.Client("Me"), "free"); + var plan = new AssignedPlan(RefToken.Client("Me"), "free"); var serialized = plan.SerializeAndDeserialize(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs index 72b71f0e0..a014663d4 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs @@ -33,9 +33,9 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var role = new Role("Name"); - var result = role.ForApp("my-app").Permissions.ToIds(); + var actual = role.ForApp("my-app").Permissions.ToIds(); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -43,9 +43,9 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var role = Role.WithPermissions("Name", "common", "common", "common"); - var result = role.ForApp("my-app").Permissions.ToIds(); + var actual = role.ForApp("my-app").Permissions.ToIds(); - Assert.Single(result); + Assert.Single(actual); } [Fact] @@ -53,9 +53,9 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var role = Role.WithPermissions("Name", "clients.read"); - var result = role.ForApp("my-app").Permissions.ToIds(); + var actual = role.ForApp("my-app").Permissions.ToIds(); - Assert.Equal("squidex.apps.my-app.clients.read", result.ElementAt(0)); + Assert.Equal("squidex.apps.my-app.clients.read", actual.ElementAt(0)); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index 87f813eb2..103875f7e 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs @@ -156,19 +156,19 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Theory] public void Should_get_default_roles(string name, int permissionCount) { - var found = roles_0.TryGet("app", name, false, out var result); + var found = roles_0.TryGet("app", name, false, out var actual); Assert.True(found); - Assert.True(result!.IsDefault); + Assert.True(actual!.IsDefault); Assert.True(roles_0.Contains(name)); - foreach (var permission in result.Permissions) + foreach (var permission in actual.Permissions) { Assert.StartsWith("squidex.apps.app.", permission.Id, StringComparison.Ordinal); Assert.DoesNotContain("{app}", permission.Id, StringComparison.Ordinal); } - Assert.Equal(permissionCount, result!.Permissions.Count); + Assert.Equal(permissionCount, actual!.Permissions.Count); } [InlineData("Developer", 15)] @@ -178,19 +178,19 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Theory] public void Should_add_extra_permissions_for_frontend_client(string name, int permissionCount) { - var found = roles_0.TryGet("app", name, true, out var result); + var found = roles_0.TryGet("app", name, true, out var actual); Assert.True(found); - Assert.Equal(permissionCount, result!.Permissions.Count); + Assert.Equal(permissionCount, actual!.Permissions.Count); } [Fact] public void Should_return_null_if_role_not_found() { - var found = roles_0.TryGet("app", "custom", false, out var result); + var found = roles_0.TryGet("app", "custom", false, out var actual); Assert.False(found); - Assert.Null(result); + Assert.Null(actual); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs index 2a8da892d..f73ced573 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs @@ -60,10 +60,10 @@ namespace Squidex.Domain.Apps.Core.Model.Assets { var sut = new AssetMetadata(); - var found = sut.TryGetByPath(string.Empty, out var result); + var found = sut.TryGetByPath(string.Empty, out var actual); Assert.False(found); - Assert.Same(sut, result); + Assert.Same(sut, actual); } [Fact] @@ -74,10 +74,10 @@ namespace Squidex.Domain.Apps.Core.Model.Assets ["someValue"] = JsonValue.Create(800) }; - var found = sut.TryGetByPath("someValue", out var result); + var found = sut.TryGetByPath("someValue", out var actual); Assert.True(found); - Assert.Equal(JsonValue.Create(800), result); + Assert.Equal(JsonValue.Create(800), actual); } [Fact] @@ -85,10 +85,10 @@ namespace Squidex.Domain.Apps.Core.Model.Assets { var sut = new AssetMetadata(); - var found = sut.TryGetByPath("notFound", out var result); + var found = sut.TryGetByPath("notFound", out var actual); Assert.False(found); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -103,10 +103,10 @@ namespace Squidex.Domain.Apps.Core.Model.Assets .Add("nested2", 12)) }; - var found = sut.TryGetByPath("meta.nested1.nested2", out var result); + var found = sut.TryGetByPath("meta.nested1.nested2", out var actual); Assert.True(found); - Assert.Equal(JsonValue.Create(12), result); + Assert.Equal(JsonValue.Create(12), actual); } [Fact] @@ -119,10 +119,10 @@ namespace Squidex.Domain.Apps.Core.Model.Assets ["string"] = JsonValue.Create(value) }; - var found = sut.TryGetString("string", out var result); + var found = sut.TryGetString("string", out var actual); Assert.True(found); - Assert.Equal(value, result); + Assert.Equal(value, actual); } [Fact] @@ -133,10 +133,10 @@ namespace Squidex.Domain.Apps.Core.Model.Assets ["string"] = JsonValue.Create(12) }; - var found = sut.TryGetString("string", out var result); + var found = sut.TryGetString("string", out var actual); Assert.False(found); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -144,10 +144,10 @@ namespace Squidex.Domain.Apps.Core.Model.Assets { var sut = new AssetMetadata(); - var found = sut.TryGetString("other", out var result); + var found = sut.TryGetString("other", out var actual); Assert.False(found); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -160,10 +160,10 @@ namespace Squidex.Domain.Apps.Core.Model.Assets ["number"] = JsonValue.Create(value) }; - var found = sut.TryGetNumber("number", out var result); + var found = sut.TryGetNumber("number", out var actual); Assert.True(found); - Assert.Equal(value, result); + Assert.Equal(value, actual); } [Fact] @@ -174,10 +174,10 @@ namespace Squidex.Domain.Apps.Core.Model.Assets ["number"] = JsonValue.Create(true) }; - var found = sut.TryGetNumber("number", out var result); + var found = sut.TryGetNumber("number", out var actual); Assert.False(found); - Assert.Equal(0, result); + Assert.Equal(0, actual); } [Fact] @@ -185,10 +185,10 @@ namespace Squidex.Domain.Apps.Core.Model.Assets { var sut = new AssetMetadata(); - var found = sut.TryGetNumber("other", out var result); + var found = sut.TryGetNumber("other", out var actual); Assert.False(found); - Assert.Equal(0, result); + Assert.Equal(0, actual); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs index bcdff3f80..73c6f5153 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs @@ -19,17 +19,17 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] public void Should_convert_from_string() { - var result = typeConverter.ConvertFromString("Draft"); + var actual = typeConverter.ConvertFromString("Draft"); - Assert.Equal(Status.Draft, result); + Assert.Equal(Status.Draft, actual); } [Fact] public void Should_convert_to_string() { - var result = typeConverter.ConvertToString(Status.Draft); + var actual = typeConverter.ConvertToString(Status.Draft); - Assert.Equal("Draft", result); + Assert.Equal("Draft", actual); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/TranslationStatusTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/TranslationStatusTests.cs index ce22a438a..20b0c21fd 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/TranslationStatusTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/TranslationStatusTests.cs @@ -22,14 +22,14 @@ namespace Squidex.Domain.Apps.Core.Model.Contents { var schema = new Schema("my-schema"); - var result = TranslationStatus.Create(new ContentData(), schema, languages); + var actual = TranslationStatus.Create(new ContentData(), schema, languages); Assert.Equal(new TranslationStatus { [Language.EN] = 100, [Language.DE] = 100, [Language.IT] = 100 - }, result); + }, actual); } [Fact] @@ -39,14 +39,14 @@ namespace Squidex.Domain.Apps.Core.Model.Contents new Schema("my-schema") .AddString(1, "field1", Partitioning.Invariant); - var result = TranslationStatus.Create(new ContentData(), schema, languages); + var actual = TranslationStatus.Create(new ContentData(), schema, languages); Assert.Equal(new TranslationStatus { [Language.EN] = 100, [Language.DE] = 100, [Language.IT] = 100 - }, result); + }, actual); } [Fact] @@ -56,14 +56,14 @@ namespace Squidex.Domain.Apps.Core.Model.Contents new Schema("my-schema") .AddString(1, "field1", Partitioning.Language); - var result = TranslationStatus.Create(new ContentData(), schema, languages); + var actual = TranslationStatus.Create(new ContentData(), schema, languages); Assert.Equal(new TranslationStatus { [Language.EN] = 0, [Language.DE] = 0, [Language.IT] = 0 - }, result); + }, actual); } [Fact] @@ -90,14 +90,14 @@ namespace Squidex.Domain.Apps.Core.Model.Contents new ContentFieldData() .AddLocalized(Language.EN, "en")); - var result = TranslationStatus.Create(data, schema, languages); + var actual = TranslationStatus.Create(data, schema, languages); Assert.Equal(new TranslationStatus { [Language.EN] = 100, [Language.DE] = 67, [Language.IT] = 0 - }, result); + }, actual); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs index 3815f0361..a10be6f1b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs @@ -75,9 +75,9 @@ namespace Squidex.Domain.Apps.Core.Model.Contents var serialized = source.SerializeAndDeserialize(); - var result = serialized.ToSource(); + var actual = serialized.ToSource(); - Assert.Equal(source.Role, result?.Roles?.Single()); + Assert.Equal(source.Role, actual?.Roles?.Single()); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs index ddefc9145..7c62fba50 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs @@ -34,9 +34,9 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] public void Should_provide_default_workflow_if_none_found() { - var result = Workflows.Empty.GetFirst(); + var actual = Workflows.Empty.GetFirst(); - Assert.Same(Workflow.Default, result); + Assert.Same(Workflow.Default, actual); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs index d9f6e6e27..f74d57db1 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs @@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .Add("nested", JsonValue.Array(1, 2)))) .Add(Component.Discriminator, DomainId.Empty)))); - var result = + var actual = new ContentConverter(components, schema) .Add(new ValueConverter()) .Convert(source); @@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new JsonObject())) .Add(Component.Discriminator, DomainId.Empty)))); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -169,7 +169,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .Add("nested", JsonValue.Array(1, 2)))) .Add(Component.Discriminator, DomainId.Empty)))); - var result = + var actual = new ContentConverter(components, schema) .Add(new ItemConverter()) .Convert(source); @@ -221,7 +221,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .Add("nested", JsonValue.Array(1, 2)))) .Add(Component.Discriminator, DomainId.Empty)))); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } private sealed class ItemConverter : IContentItemConverter diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs index d4d2a587f..32d1a5f23 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs @@ -47,14 +47,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddLocalized("en", JsonValue.Null) .AddLocalized("de", 1)); - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ExcludeChangedTypes(TestUtils.DefaultSerializer)) .Convert(source); var expected = source; - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddLocalized("en", "2") .AddLocalized("de", 2)); - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ExcludeChangedTypes(TestUtils.DefaultSerializer)) .Convert(source); @@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new ContentFieldData() .AddLocalized("en", 1)); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddLocalized("en", JsonValue.Null) .AddLocalized("de", 1)); - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(ExcludeHidden.Instance) .Convert(source); @@ -124,7 +124,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new ContentFieldData() .AddLocalized("en", 1)); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -148,7 +148,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new ContentFieldData() .AddLocalized("en", "2")); - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ExcludeChangedTypes(TestUtils.DefaultSerializer)) .Convert(source); @@ -159,7 +159,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new ContentFieldData() .AddLocalized("en", 1)); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddLocalized("de", "DE") .AddLocalized("it", "IT")); - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ResolveLanguages(languages)) .Convert(source); @@ -191,7 +191,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddLocalized("en", "EN") .AddLocalized("de", "DE")); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -211,7 +211,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddLocalized("de", "DE") .AddLocalized("it", "IT")); - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ResolveLanguages(languages, true, new[] { Language.DE })) .Convert(source); @@ -222,7 +222,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new ContentFieldData() .AddLocalized("de", "DE")); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Theory] @@ -247,7 +247,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent source[field1.Name]!["en"] = value!; } - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ResolveLanguages(languages)) .Convert(source); @@ -258,7 +258,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new ContentFieldData() .AddLocalized("en", "A")); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Theory] @@ -283,7 +283,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent source[field1.Name]!["en"] = value; } - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ResolveLanguages(languages)) .Convert(source); @@ -298,7 +298,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent expected[field1.Name]!["en"] = value; } - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -316,14 +316,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new ContentFieldData() .AddInvariant("A")); - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ResolveLanguages(languages)) .Convert(source); var expected = source; - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Theory] @@ -348,7 +348,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent source[field1.Name]![InvariantPartitioning.Key] = value; } - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ResolveInvariant(languages)) .Convert(source); @@ -359,7 +359,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new ContentFieldData() .AddInvariant("EN")); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Theory] @@ -384,7 +384,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent source[field1.Name]![InvariantPartitioning.Key] = value; } - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ResolveInvariant(languages)) .Convert(source); @@ -395,7 +395,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new ContentFieldData() .AddInvariant("DE")); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -412,14 +412,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddField(field1.Name, new ContentFieldData()); - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ResolveLanguages(languages)) .Convert(source); var expected = source; - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Theory] @@ -450,7 +450,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent source[field1.Name]!["de"] = value!; } - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ResolveLanguages(config)) .Convert(source); @@ -464,7 +464,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddLocalized("es", "IT") .AddLocalized("de", "EN")); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -483,7 +483,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddLocalized("en", "EN") .AddLocalized("de", "DE")); - var result = + var actual = new ContentConverter(ResolvedComponents.Empty, schema) .Add(new ResolveLanguages(languages, true, Language.IT)) .Convert(source); @@ -494,7 +494,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new ContentFieldData() .AddLocalized("en", "EN")); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -504,11 +504,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent var source = new ContentFieldData(); - var result = + var actual = new ResolveLanguages(languages) .ConvertFieldAfter(field, source); - Assert.Same(source, result); + Assert.Same(source, actual); } [Fact] @@ -518,11 +518,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent var source = new ContentFieldData(); - var result = + var actual = new ResolveLanguages(languages, true, Array.Empty()) .ConvertFieldAfter(field, source); - Assert.Same(source, result); + Assert.Same(source, actual); } [Fact] @@ -532,11 +532,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent var source = new ContentFieldData(); - var result = + var actual = new ResolveLanguages(languages) .ConvertFieldAfter(field, source); - Assert.Same(source, result); + Assert.Same(source, actual); } /* @@ -565,11 +565,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .Add(Component.Discriminator, componentId) .Add("schemaName", component.Name)); - var result = FieldConverters.AddSchemaName(components)(data, field); + var actual = FieldConverters.AddSchemaName(components)(data, field); var expected = new ContentFieldData(); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); }*/ } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs index 535459c97..8b3c76dbb 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent id1, id2); - var (_, result) = + var (_, actual) = new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) .ConvertValue(field, source, null); @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent $"url/to/{id1}", $"url/to/{id2}"); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Theory] @@ -95,13 +95,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent id1, id2); - var (_, result) = + var (_, actual) = new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) .ConvertValue(field, source, null); var expected = source; - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Theory] @@ -116,7 +116,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent id1, id2); - var (_, result) = + var (_, actual) = new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) .ConvertValue(field.FieldsByName["assets"], source, field); @@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent $"url/to/{id1}", $"url/to/{id2}"); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Theory] @@ -142,13 +142,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent id1, id2); - var (_, result) = + var (_, actual) = new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) .ConvertValue(field, source, null); var expected = source; - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -167,7 +167,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new JsonObject() .Add(Component.Discriminator, componentId); - var result = + var actual = new AddSchemaNames(components) .ConvertItem(field, source); @@ -176,7 +176,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .Add(Component.Discriminator, componentId) .Add("schemaName", component.Name); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -196,13 +196,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .Add(Component.Discriminator, componentId) .Add("schemaName", "existing"); - var result = + var actual = new AddSchemaNames(components) .ConvertItem(field, source); var expected = source; - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -221,13 +221,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new JsonObject() .Add(Component.Discriminator, componentId); - var result = + var actual = new AddSchemaNames(components) .ConvertItem(field, source); var expected = source; - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -245,13 +245,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent var source = new JsonObject(); - var result = + var actual = new AddSchemaNames(components) .ConvertItem(field, source); var expected = source; - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -265,13 +265,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent new JsonObject() .Add(Component.Discriminator, componentId); - var result = + var actual = new AddSchemaNames(ResolvedComponents.Empty) .ConvertItem(field, source); var expected = source; - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs index 949229a41..e4ec87e38 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs @@ -190,12 +190,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds .Add("nestedAssets", JsonValue.Array(id2)))) .Add(Component.Discriminator, DomainId.Empty)))); - var result = + var actual = new ContentConverter(components, schema) .Add(new ValueReferencesConverter(new HashSet { id2 })) .Convert(source); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -203,9 +203,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds { var sut = Fields.String(1, "myString", Partitioning.Invariant); - var result = sut.GetReferencedIds(JsonValue.Create("invalid"), components).ToArray(); + var actual = sut.GetReferencedIds(JsonValue.Create("invalid"), components).ToArray(); - Assert.Empty(result); + Assert.Empty(actual); } [Theory] @@ -222,45 +222,45 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds new JsonObject() .Add(field.Name, CreateValue(id1, id2))); - var result = arrayField.GetReferencedIds(value, components).ToArray(); + var actual = arrayField.GetReferencedIds(value, components).ToArray(); - Assert.Equal(new[] { id1, id2 }, result); + Assert.Equal(new[] { id1, id2 }, actual); } [Theory] [MemberData(nameof(ReferencingFields))] public void Should_return_empty_list_from_field_if_value_item_is_invalid(IField field) { - var result = field.GetReferencedIds(JsonValue.Array(1), components).ToArray(); + var actual = field.GetReferencedIds(JsonValue.Array(1), components).ToArray(); - Assert.Empty(result); + Assert.Empty(actual); } [Theory] [MemberData(nameof(ReferencingFields))] public void Should_return_empty_list_from_field_if_value_is_empty(IField field) { - var result = field.GetReferencedIds(new JsonArray(), components).ToArray(); + var actual = field.GetReferencedIds(new JsonArray(), components).ToArray(); - Assert.Empty(result); + Assert.Empty(actual); } [Theory] [MemberData(nameof(ReferencingFields))] public void Should_return_empty_list_from_field_if_value_is_json_null(IField field) { - var result = field.GetReferencedIds(default, components).ToArray(); + var actual = field.GetReferencedIds(default, components).ToArray(); - Assert.Empty(result); + Assert.Empty(actual); } [Theory] [MemberData(nameof(ReferencingFields))] public void Should_return_empty_list_from_field_if_value_is_null(IField field) { - var result = field.GetReferencedIds(default, components).ToArray(); + var actual = field.GetReferencedIds(default, components).ToArray(); - Assert.Empty(result); + Assert.Empty(actual); } [Theory] @@ -272,18 +272,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds var value = CreateValue(id1, id2); - var result = field.GetReferencedIds(value, components); + var actual = field.GetReferencedIds(value, components); - Assert.Equal(new HashSet { id1, id2 }, result); + Assert.Equal(new HashSet { id1, id2 }, actual); } [Theory] [MemberData(nameof(ReferencingFields))] public void Should_return_same_value_from_field_if_value_is_json_null(IField field) { - var (_, result) = new ValueReferencesConverter(RandomIds()).ConvertValue(field, JsonValue.Null, null); + var (_, actual) = new ValueReferencesConverter(RandomIds()).ConvertValue(field, JsonValue.Null, null); - Assert.Equal(JsonValue.Null, result); + Assert.Equal(JsonValue.Null, actual); } [Theory] @@ -295,9 +295,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds var value = CreateValue(id1, id2); - var (_, result) = new ValueReferencesConverter(HashSet.Of(id1)).ConvertValue(field, value, null); + var (_, actual) = new ValueReferencesConverter(HashSet.Of(id1)).ConvertValue(field, value, null); - Assert.Equal(CreateValue(id1), result); + Assert.Equal(CreateValue(id1), actual); } public static IEnumerable ReferencingNestedFields() diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs index fa142fc8b..dd88c3325 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs @@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateFilters private static HashSet AllPropertyNames(FilterSchema schema) { - var result = new HashSet(); + var actual = new HashSet(); void AddProperties(FilterSchema current) { @@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateFilters foreach (var field in current.Fields.OrEmpty()) { - result.Add(field.Path); + actual.Add(field.Path); AddProperties(field.Schema); } @@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateFilters AddProperties(schema); - return result; + return actual; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs index e65858341..b857a3ce2 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs @@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema private static HashSet AllPropertyNames(JsonSchema schema) { - var result = new HashSet(); + var actual = new HashSet(); void AddProperties(JsonSchema current) { @@ -96,7 +96,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema foreach (var (key, value) in current.Properties.OrEmpty()) { - result.Add(key); + actual.Add(key); AddProperties(value); } @@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema AddProperties(schema); - return result; + return actual; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs index 5ee34107b..81f14d55f 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs @@ -126,9 +126,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal($"Name my-app has id {appId.Id}", result); + Assert.Equal($"Name my-app has id {appId.Id}", actual); } [Theory] @@ -142,9 +142,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, SchemaId = schemaId }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal($"Name my-schema has id {schemaId.Id}", result); + Assert.Equal($"Name my-schema has id {schemaId.Id}", actual); } [Theory] @@ -158,9 +158,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, Timestamp = now }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal($"DateTime: {now:yyyy-MM-dd-hh-mm-ss}", result); + Assert.Equal($"DateTime: {now:yyyy-MM-dd-hh-mm-ss}", actual); } [Theory] @@ -174,9 +174,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, Timestamp = now }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal($"Date: {now:yyyy-MM-dd}", result); + Assert.Equal($"Date: {now:yyyy-MM-dd}", actual); } [Theory] @@ -190,9 +190,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedCommentEvent { AppId = appId, MentionedUser = user }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("From me (me@email.com, user123)", result); + Assert.Equal("From me (me@email.com, user123)", actual); } [Theory] @@ -206,9 +206,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, User = user }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("From me (me@email.com, user123)", result); + Assert.Equal("From me (me@email.com, user123)", actual); } [Theory] @@ -222,9 +222,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("From null (null, null)", result); + Assert.Equal("From null (null, null)", actual); } [Theory] @@ -238,9 +238,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, User = new ClientUser(RefToken.Client("android")) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("From android (client:android, android)", result); + Assert.Equal("From android (client:android, android)", actual); } [Theory] @@ -254,9 +254,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, Version = 13 }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Version: 13", result); + Assert.Equal("Version: 13", actual); } [Theory] @@ -270,9 +270,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, FileName = "my-file.png" }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("File: my-file.png", result); + Assert.Equal("File: my-file.png", actual); } [Theory] @@ -286,9 +286,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, AssetType = AssetType.Audio }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Type: Audio", result); + Assert.Equal("Type: Audio", actual); } [Theory] @@ -303,9 +303,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Download at asset-content-url", result); + Assert.Equal("Download at asset-content-url", actual); } [Theory] @@ -320,9 +320,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Download at null", result); + Assert.Equal("Download at null", actual); } [Theory] @@ -337,9 +337,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Download at asset-content-url", result); + Assert.Equal("Download at asset-content-url", actual); } [Theory] @@ -354,9 +354,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Download at null", result); + Assert.Equal("Download at null", actual); } [Theory] @@ -371,9 +371,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Download at asset-content-slug-url", result); + Assert.Equal("Download at asset-content-slug-url", actual); } [Theory] @@ -388,9 +388,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Download at null", result); + Assert.Equal("Download at null", actual); } [Theory] @@ -404,9 +404,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Go to content-url", result); + Assert.Equal("Go to content-url", actual); } [Theory] @@ -420,9 +420,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Go to null", result); + Assert.Equal("Go to null", actual); } [Theory] @@ -436,9 +436,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, Status = Status.Published }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Published", result); + Assert.Equal("Published", actual); } [Theory] @@ -452,9 +452,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("null", result); + Assert.Equal("null", actual); } [Theory] @@ -468,9 +468,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, Type = EnrichedContentEventType.Created }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Created", result); + Assert.Equal("Created", actual); } [Theory] @@ -484,9 +484,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("null", result); + Assert.Equal("null", actual); } [Theory] @@ -508,9 +508,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddLocalized("zh-TW", "Berlin")) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Berlin", result); + Assert.Equal("Berlin", actual); } [Theory] @@ -532,9 +532,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant("Berlin")) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("null", result); + Assert.Equal("null", actual); } [Theory] @@ -556,9 +556,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant("Berlin")) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("null", result); + Assert.Equal("null", actual); } [Theory] @@ -580,9 +580,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant(new JsonArray())) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("null", result); + Assert.Equal("null", actual); } [Theory] @@ -604,9 +604,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant(new JsonObject().Add("name", "Berlin"))) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("null", result); + Assert.Equal("null", actual); } [Theory] @@ -628,9 +628,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant("Berlin")) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Berlin", result); + Assert.Equal("Berlin", actual); } [Theory] @@ -652,9 +652,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant(JsonValue.Array("Berlin"))) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Berlin", result); + Assert.Equal("Berlin", actual); } [Theory] @@ -676,9 +676,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant(new JsonObject().Add("name", "Berlin"))) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Berlin", result); + Assert.Equal("Berlin", actual); } [Theory] @@ -700,9 +700,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant(new JsonObject().Add("name", "Berlin"))) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("{\"name\":\"Berlin\"}", result); + Assert.Equal("{\"name\":\"Berlin\"}", actual); } [Theory] @@ -724,9 +724,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant(JsonValue.Array(1, 2, 3))) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("[1,2,3]", result?.Replace(" ", string.Empty, StringComparison.Ordinal)); + Assert.Equal("[1,2,3]", actual?.Replace(" ", string.Empty, StringComparison.Ordinal)); } [Theory] @@ -748,9 +748,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant(new JsonObject().Add("name", "Berlin"))) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("{\"city\":{\"iv\":{\"name\":\"Berlin\"}}}", result); + Assert.Equal("{\"city\":{\"iv\":{\"name\":\"Berlin\"}}}", actual); } [Theory] @@ -764,9 +764,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, Actor = RefToken.Client("android") }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("From client:android", result); + Assert.Equal("From client:android", actual); } [Theory] @@ -780,9 +780,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, LastModified = Instant.FromUnixTimeSeconds(1590769584) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("1590769584000", result); + Assert.Equal("1590769584000", actual); } [Theory] @@ -803,9 +803,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant("2020-06-01T10:10:20Z")) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("1591006220000", result); + Assert.Equal("1591006220000", actual); } [Theory] @@ -819,9 +819,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, LastModified = Instant.FromUnixTimeSeconds(1590769584) }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("1590769584", result); + Assert.Equal("1590769584", actual); } [Theory] @@ -838,9 +838,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => user.Claims) .Returns(new List { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("DONALD DUCK", result); + Assert.Equal("DONALD DUCK", actual); } [Theory] @@ -857,9 +857,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => user.Claims) .Returns(new List { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("donald duck", result); + Assert.Equal("donald duck", actual); } [Theory] @@ -876,9 +876,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => user.Claims) .Returns(new List { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck ") }); - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Donald Duck", result); + Assert.Equal("Donald Duck", actual); } [Theory] @@ -895,9 +895,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => user.Claims) .Returns(new List { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("donald-duck", result); + Assert.Equal("donald-duck", actual); } [Theory] @@ -914,9 +914,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => user.Claims) .Returns(new List { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck ") }); - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("DONALD DUCK", result); + Assert.Equal("DONALD DUCK", actual); } [Theory] @@ -933,9 +933,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => user.Claims) .Returns(new List { new Claim(SquidexClaimTypes.DisplayName, "Donald\"Duck") }); - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Donald\\\"Duck", result); + Assert.Equal("Donald\\\"Duck", actual); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index 84e3ef024..f2280c982 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -108,9 +108,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public void Should_serialize_object_to_json() { - var result = sut.ToPayload(new { Value = 1 }); + var actual = sut.ToPayload(new { Value = 1 }); - Assert.NotNull(result); + Assert.NotNull(actual); } [Fact] @@ -118,9 +118,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId }; - var result = sut.ToPayload(@event); + var actual = sut.ToPayload(@event); - Assert.NotNull(result); + Assert.NotNull(actual); } [Fact] @@ -128,9 +128,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, Name = "MyEventName" }; - var result = sut.ToEnvelope(@event); + var actual = sut.ToEnvelope(@event); - Assert.Contains("MyEventName", result, StringComparison.Ordinal); + Assert.Contains("MyEventName", actual, StringComparison.Ordinal); } [Fact] @@ -145,9 +145,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddInvariant(new JsonArray())) }; - var result = await sut.FormatAsync("${CONTENT_DATA.city.iv.data.name}", @event); + var actual = await sut.FormatAsync("${CONTENT_DATA.city.iv.data.name}", @event); - Assert.Equal("Reference", result); + Assert.Equal("Reference", actual); } [Theory] @@ -156,9 +156,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, FileName = null! }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, result); + Assert.Equal(expect, actual); } [Theory] @@ -167,9 +167,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, FileName = null! }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, result); + Assert.Equal(expect, actual); } [Theory] @@ -180,9 +180,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, FileName = "Donald Duck" }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, result); + Assert.Equal(expect, actual); } [Theory] @@ -196,9 +196,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, FileName = name }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, result); + Assert.Equal(expect, actual); } [Theory] @@ -215,9 +215,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => user.Claims) .Returns(new List { new Claim(SquidexClaimTypes.DisplayName, name) }); - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, result); + Assert.Equal(expect, actual); } [Theory] @@ -227,9 +227,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedAssetEvent { AppId = appId, FileName = "Donald Duck" }; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, result); + Assert.Equal(expect, actual); } [Fact] @@ -237,9 +237,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, Actor = RefToken.Client("android") }; - var result = await sut.FormatAsync("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); + var actual = await sut.FormatAsync("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); - Assert.Equal("{\"actor\":\"client:android\"}", result); + Assert.Equal("{\"actor\":\"client:android\"}", actual); } [Fact] @@ -247,9 +247,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, Actor = RefToken.Client("mobile\"android") }; - var result = await sut.FormatAsync("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); + var actual = await sut.FormatAsync("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); - Assert.Equal("{\"actor\":\"client:mobile\\\"android\"}", result); + Assert.Equal("{\"actor\":\"client:mobile\\\"android\"}", actual); } [Fact] @@ -257,9 +257,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, Type = EnrichedContentEventType.Created }; - var result = await sut.FormatAsync(" Script(`${event.type}`)", @event); + var actual = await sut.FormatAsync(" Script(`${event.type}`)", @event); - Assert.Equal("Created", result); + Assert.Equal("Created", actual); } [Fact] @@ -267,9 +267,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var @event = new EnrichedContentEvent { AppId = appId, Type = EnrichedContentEventType.Created }; - var result = await sut.FormatAsync("Script(`${event.type}`) ", @event); + var actual = await sut.FormatAsync("Script(`${event.type}`) ", @event); - Assert.Equal("Created", result); + Assert.Equal("Created", actual); } [Fact] @@ -291,9 +291,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules 'categories': event.data.categories.iv }))"; - var result = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("{'categories':['ref1','ref2','ref3']}", result? + Assert.Equal("{'categories':['ref1','ref2','ref3']}", actual? .Replace(" ", string.Empty, StringComparison.Ordinal) .Replace("\"", "'", StringComparison.Ordinal)); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index d0fb53bf3..26eee7b6a 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -147,9 +147,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var context = RuleInvalidTrigger(); - var result = sut.CanCreateSnapshotEvents(context); + var actual = sut.CanCreateSnapshotEvents(context); - Assert.False(result); + Assert.False(actual); } [Fact] @@ -160,9 +160,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) .Returns(false); - var result = sut.CanCreateSnapshotEvents(context); + var actual = sut.CanCreateSnapshotEvents(context); - Assert.False(result); + Assert.False(actual); } [Fact] @@ -173,9 +173,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) .Returns(true); - var result = sut.CanCreateSnapshotEvents(context); + var actual = sut.CanCreateSnapshotEvents(context); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -320,9 +320,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var eventEnvelope = CreateEnvelope(new InvalidEvent()); - var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - Assert.Equal(SkipReason.WrongEvent, result.SkipReason); + Assert.Equal(SkipReason.WrongEvent, actual.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); @@ -389,9 +389,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var eventEnvelope = CreateEnvelope(new ContentCreated()); - var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - Assert.Equal(SkipReason.Disabled, result.SkipReason); + Assert.Equal(SkipReason.Disabled, actual.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); @@ -405,9 +405,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var eventEnvelope = CreateEnvelope(new ContentCreated()); var eventEnriched = SetupFullFlow(context, eventEnvelope); - var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - AssertJob(eventEnriched, result, SkipReason.Disabled); + AssertJob(eventEnriched, actual, SkipReason.Disabled); } [Fact] @@ -440,9 +440,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); var eventEnriched = SetupFullFlow(context, eventEnvelope); - var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - AssertJob(eventEnriched, result, SkipReason.None); + AssertJob(eventEnriched, actual, SkipReason.None); } [Fact] @@ -455,9 +455,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); var eventEnriched = SetupFullFlow(context, eventEnvelope); - var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - AssertJob(eventEnriched, result, SkipReason.TooOld); + AssertJob(eventEnriched, actual, SkipReason.TooOld); } [Fact] @@ -486,9 +486,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var eventEnvelope = CreateEnvelope(new ContentCreated { FromRule = true }); var eventEnriched = SetupFullFlow(context, eventEnvelope); - var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - AssertJob(eventEnriched, result, SkipReason.FromRule); + AssertJob(eventEnriched, actual, SkipReason.FromRule); } [Fact] @@ -526,9 +526,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) .Returns(false); - var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - AssertJob(eventEnriched, result, SkipReason.ConditionPrecheckDoesNotMatch); + AssertJob(eventEnriched, actual, SkipReason.ConditionPrecheckDoesNotMatch); } [Fact] @@ -719,12 +719,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A._)) .Returns(Result.Success(actionDump)); - var result = await sut.InvokeAsync(actionName, actionData); + var actual = await sut.InvokeAsync(actionName, actionData); - Assert.Equal(RuleResult.Success, result.Result.Status); + Assert.Equal(RuleResult.Success, actual.Result.Status); - Assert.True(result.Elapsed >= TimeSpan.Zero); - Assert.True(result.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + Assert.True(actual.Elapsed >= TimeSpan.Zero); + Assert.True(actual.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); } [Fact] @@ -733,12 +733,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A._)) .Returns(Result.Failed(new InvalidOperationException(), actionDump)); - var result = await sut.InvokeAsync(actionName, actionData); + var actual = await sut.InvokeAsync(actionName, actionData); - Assert.Equal(RuleResult.Failed, result.Result.Status); + Assert.Equal(RuleResult.Failed, actual.Result.Status); - Assert.True(result.Elapsed >= TimeSpan.Zero); - Assert.True(result.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + Assert.True(actual.Elapsed >= TimeSpan.Zero); + Assert.True(actual.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); } [Fact] @@ -747,14 +747,14 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A._)) .Returns(Result.Failed(new TimeoutException(), actionDump)); - var result = await sut.InvokeAsync(actionName, actionData); + var actual = await sut.InvokeAsync(actionName, actionData); - Assert.Equal(RuleResult.Timeout, result.Result.Status); + Assert.Equal(RuleResult.Timeout, actual.Result.Status); - Assert.True(result.Elapsed >= TimeSpan.Zero); - Assert.True(result.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + Assert.True(actual.Elapsed >= TimeSpan.Zero); + Assert.True(actual.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); - Assert.True(result.Result.Dump?.IndexOf("Action timed out.", StringComparison.OrdinalIgnoreCase) >= 0); + Assert.True(actual.Result.Dump?.IndexOf("Action timed out.", StringComparison.OrdinalIgnoreCase) >= 0); } [Fact] @@ -765,9 +765,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A._)) .Throws(ex); - var result = await sut.InvokeAsync(actionName, actionData); + var actual = await sut.InvokeAsync(actionName, actionData); - Assert.Equal(ex, result.Result.Exception); + Assert.Equal(ex, actual.Result.Exception); } private EnrichedContentEvent SetupFullFlow(RuleContext context, Envelope eventEnvelope) where T : AppEvent @@ -843,15 +843,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules return A>.That.Matches(x => x.Payload == eventEnvelope.Payload); } - private void AssertJob(EnrichedContentEvent eventEnriched, JobResult result, SkipReason skipped) + private void AssertJob(EnrichedContentEvent eventEnriched, JobResult actual, SkipReason skipped) { var now = clock.GetCurrentInstant(); - var job = result.Job!; + var job = actual.Job!; - Assert.Equal(skipped, result.SkipReason); + Assert.Equal(skipped, actual.SkipReason); - Assert.Equal(eventEnriched, result.EnrichedEvent); + Assert.Equal(eventEnriched, actual.EnrichedEvent); Assert.Equal(eventEnriched.AppId.Id, job.AppId); Assert.Equal(actionData, job.ActionData); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs index 5961a93e4..265f068eb 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs @@ -31,9 +31,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.number = { iv: 1 } "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -51,9 +51,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.number.iv = 1 "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -71,9 +71,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting Object.defineProperty(data, 'number', { value: { iv: 1 } }) "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -103,9 +103,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting delete data.number "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -127,9 +127,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.string.iv = data.string.iv + 'new' "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -151,9 +151,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.number.iv = data.number.iv + 2 "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -175,9 +175,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.boolean.iv = !data.boolean.iv "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -199,9 +199,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.number.iv = [data.number.iv[0], data.number.iv[1] + 2, 5] "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -223,9 +223,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.geo.iv = { lat: data.geo.iv.lat, lon: data.geo.iv.lat + 3 } "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -250,9 +250,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.geo.iv = nested "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -271,9 +271,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.geo.iv = nested "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Same(original, result); + Assert.Same(original, actual); } [Fact] @@ -294,9 +294,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting Object.defineProperty(data.number, 'iv', { value: 1 }) "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -317,9 +317,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting delete data.string.iv "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -341,20 +341,20 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting engine.SetValue("data", new ContentDataObject(engine, content)); const string script = @" - var result = []; + var actual = []; for (var x in data) { var field = data[x]; for (var y in field) { - result.push(field[y]); + actual.push(field[y]); } } - result; + actual; "; - var result = engine.Evaluate(script).ToObject(); + var actual = engine.Evaluate(script).ToObject(); - Assert.Equal(new[] { "1", "2", "3", "4" }, result); + Assert.Equal(new[] { "1", "2", "3", "4" }, actual); } [Fact] @@ -395,9 +395,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting data.number.iv = {input}; "; - var result = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -419,9 +419,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting engine.SetValue("data", value); engine.Execute(script); - value.TryUpdate(out var result); + value.TryUpdate(out var actual); - return result; + return actual; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs index cbb900407..b6802c391 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs @@ -56,9 +56,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return html2Text(value); "; - var result = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("Hello World", result); + Assert.Equal("Hello World", actual); } [Fact] @@ -73,9 +73,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return markdown2Text(value); "; - var result = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("Hello World", result); + Assert.Equal("Hello World", actual); } [Fact] @@ -90,9 +90,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return wordCount(value); "; - var result = sut.Execute(vars, script).AsNumber; + var actual = sut.Execute(vars, script).AsNumber; - Assert.Equal(2, result); + Assert.Equal(2, actual); } [Fact] @@ -107,9 +107,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return characterCount(value); "; - var result = sut.Execute(vars, script).AsNumber; + var actual = sut.Execute(vars, script).AsNumber; - Assert.Equal(10, result); + Assert.Equal(10, actual); } [Fact] @@ -124,9 +124,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return toCamelCase(value); "; - var result = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("helloWorld", result); + Assert.Equal("helloWorld", actual); } [Fact] @@ -141,9 +141,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return toPascalCase(value); "; - var result = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("HelloWorld", result); + Assert.Equal("HelloWorld", actual); } [Fact] @@ -158,9 +158,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return slugify(value); "; - var result = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("4-haeuser", result); + Assert.Equal("4-haeuser", actual); } [Fact] @@ -175,9 +175,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return slugify(value, true); "; - var result = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("4-hauser", result); + Assert.Equal("4-hauser", actual); } [Fact] @@ -192,9 +192,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return sha256(value); "; - var result = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("HelloWorld".ToSha256(), result); + Assert.Equal("HelloWorld".ToSha256(), actual); } [Fact] @@ -209,9 +209,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return sha512(value); "; - var result = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("HelloWorld".ToSha512(), result); + Assert.Equal("HelloWorld".ToSha512(), actual); } [Fact] @@ -226,9 +226,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return md5(value); "; - var result = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("HelloWorld".ToMD5(), result); + Assert.Equal("HelloWorld".ToMD5(), actual); } [Fact] @@ -242,9 +242,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return guid(); "; - var result = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.True(Guid.TryParse(result, out _)); + Assert.True(Guid.TryParse(actual, out _)); } [Fact] @@ -339,8 +339,8 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting }; const string script = @" - getJSON(null, function(result) { - complete(result); + getJSON(null, function(actual) { + complete(actual); }); "; @@ -375,19 +375,19 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting const string script = @" var url = 'http://squidex.io'; - getJSON(url, function(result) { - complete(result); + getJSON(url, function(actual) { + complete(actual); }); "; - var result = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); httpHandler.ShouldBeMethod(HttpMethod.Get); httpHandler.ShouldBeUrl("http://squidex.io/"); var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, result); + Assert.Equal(expectedResult, actual); } [Fact] @@ -407,12 +407,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var url = 'http://squidex.io'; - getJSON(url, function(result) { - complete(result); + getJSON(url, function(actual) { + complete(actual); }, headers); "; - var result = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); httpHandler.ShouldBeMethod(HttpMethod.Get); httpHandler.ShouldBeUrl("http://squidex.io/"); @@ -421,7 +421,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, result); + Assert.Equal(expectedResult, actual); } [Fact] @@ -436,19 +436,19 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting const string script = @" var url = 'http://squidex.io'; - deleteJSON(url, function(result) { - complete(result); + deleteJSON(url, function(actual) { + complete(actual); }); "; - var result = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); httpHandler.ShouldBeMethod(HttpMethod.Delete); httpHandler.ShouldBeUrl("http://squidex.io/"); var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, result); + Assert.Equal(expectedResult, actual); } [Fact] @@ -465,12 +465,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var body = { key: 42 }; - patchJSON(url, body, function(result) { - complete(result); + patchJSON(url, body, function(actual) { + complete(actual); }); "; - var result = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); httpHandler.ShouldBeMethod(HttpMethod.Patch); httpHandler.ShouldBeUrl("http://squidex.io/"); @@ -478,7 +478,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, result); + Assert.Equal(expectedResult, actual); } [Fact] @@ -495,12 +495,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var body = { key: 42 }; - postJSON(url, body, function(result) { - complete(result); + postJSON(url, body, function(actual) { + complete(actual); }); "; - var result = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); httpHandler.ShouldBeMethod(HttpMethod.Post); httpHandler.ShouldBeUrl("http://squidex.io/"); @@ -508,7 +508,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, result); + Assert.Equal(expectedResult, actual); } [Fact] @@ -525,12 +525,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var body = { key: 42 }; - putJSON(url, body, function(result) { - complete(result); + putJSON(url, body, function(actual) { + complete(actual); }); "; - var result = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); httpHandler.ShouldBeMethod(HttpMethod.Put); httpHandler.ShouldBeUrl("http://squidex.io/"); @@ -538,7 +538,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, result); + Assert.Equal(expectedResult, actual); } private MockupHttpHandler SetupRequest() diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs index 6defd159d..6b1dd5f9d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs @@ -97,9 +97,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting x => x "; - var result = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -138,9 +138,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting replace(data); "; - var result = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -180,9 +180,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var x = 0; "; - var result = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -196,14 +196,14 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting const string script = @" var x = 0; - getJSON('http://mockup.squidex.io', function(result) { + getJSON('http://mockup.squidex.io', function(actual) { complete(); }); "; - var result = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -232,9 +232,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting replace(data); "; - var result = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -258,17 +258,17 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting const string script = @" var data = ctx.data; - getJSON('http://mockup.squidex.io', function(result) { - data.operation = { iv: result.key }; + getJSON('http://mockup.squidex.io', function(actual) { + data.operation = { iv: actual.key }; replace(data); }); "; - var result = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -284,17 +284,17 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting const string script = @" var data = ctx.data; - getJSON('http://mockup.squidex.io', function(result) { - data.operation = { iv: result.key }; + getJSON('http://mockup.squidex.io', function(actual) { + data.operation = { iv: actual.key }; replace(data); }); "; - var result = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.NotEmpty(result); + Assert.NotEmpty(actual); } [Fact] @@ -310,8 +310,8 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting const string script = @" var data = ctx.data; - getJSON('http://cloud.squidex.io/healthz', function(result) { - data.operation = { iv: result.key }; + getJSON('http://cloud.squidex.io/healthz', function(actual) { + data.operation = { iv: actual.key }; }); "; @@ -354,9 +354,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting replace(data); "; - var result = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -398,9 +398,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting replace(ctx.data); "; - var result = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -415,9 +415,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting value.i == 2 "; - var result = ((IScriptEngine)sut).Evaluate(vars, script); + var actual = ((IScriptEngine)sut).Evaluate(vars, script); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -432,9 +432,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting value.status == 'Published' "; - var result = ((IScriptEngine)sut).Evaluate(vars, script); + var actual = ((IScriptEngine)sut).Evaluate(vars, script); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -449,9 +449,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting value.i == 3 "; - var result = ((IScriptEngine)sut).Evaluate(vars, script); + var actual = ((IScriptEngine)sut).Evaluate(vars, script); - Assert.False(result); + Assert.False(actual); } [Fact] @@ -466,9 +466,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting function(); "; - var result = ((IScriptEngine)sut).Evaluate(vars, script); + var actual = ((IScriptEngine)sut).Evaluate(vars, script); - Assert.False(result); + Assert.False(actual); } [Fact] @@ -485,9 +485,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting return value; "; - var result = sut.Execute(vars, script); + var actual = sut.Execute(vars, script); - Assert.Equal(id.ToString(), result.ToString()); + Assert.Equal(id.ToString(), actual.ToString()); } [Fact] @@ -508,9 +508,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting sut.Execute(vars, script1, new ScriptOptions { AsContext = true }); - var result = sut.Execute(vars, script2, new ScriptOptions { AsContext = true }); + var actual = sut.Execute(vars, script2, new ScriptOptions { AsContext = true }); - Assert.Equal(JsonValue.Create(28), result); + Assert.Equal(JsonValue.Create(28), actual); } [Fact] @@ -531,9 +531,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting sut.Execute(vars, script1, new ScriptOptions { AsContext = true }); - var result = sut.Execute(vars, script2, new ScriptOptions { AsContext = true }); + var actual = sut.Execute(vars, script2, new ScriptOptions { AsContext = true }); - Assert.Equal(JsonValue.Create(28), result); + Assert.Equal(JsonValue.Create(28), actual); } [Fact] @@ -570,9 +570,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting } } - var result = await sut.TransformAsync(vars2, script2, new ScriptOptions { AsContext = true }); + var actual = await sut.TransformAsync(vars2, script2, new ScriptOptions { AsContext = true }); - Assert.Equal(JsonValue.Create(28), result["test"]!["iv"]); + Assert.Equal(JsonValue.Create(28), actual["test"]!["iv"]); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs index 2168cfce7..f55b08fb8 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs @@ -48,9 +48,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting [Fact] public void Should_describe_content_script() { - var result = sut.ContentScript(dataSchema); + var actual = sut.ContentScript(dataSchema); - AssertCompletion(result, + AssertCompletion(actual, PresetUser("ctx.user"), new[] { @@ -81,9 +81,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting [Fact] public void Should_describe_asset_script() { - var result = sut.AssetScript(); + var actual = sut.AssetScript(); - AssertCompletion(result, + AssertCompletion(actual, PresetUser("ctx.user"), new[] { @@ -124,22 +124,22 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting [Fact] public void Should_describe_content_trigger() { - var result = sut.ContentTrigger(dataSchema); + var actual = sut.ContentTrigger(dataSchema); - AssertContentTrigger(result); + AssertContentTrigger(actual); } [Fact] public void Should_describe_dynamic_content_trigger() { - var result = sut.Trigger("ContentChanged"); + var actual = sut.Trigger("ContentChanged"); - AssertContentTrigger(result); + AssertContentTrigger(actual); } - private static void AssertContentTrigger(IReadOnlyList result) + private static void AssertContentTrigger(IReadOnlyList actual) { - AssertCompletion(result, + AssertCompletion(actual, PresetActor("event.actor"), PresetUser("event.user"), new[] @@ -178,22 +178,22 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting [Fact] public void Should_describe_asset_trigger() { - var result = sut.AssetTrigger(); + var actual = sut.AssetTrigger(); - AssertAssetTrigger(result); + AssertAssetTrigger(actual); } [Fact] public void Should_describe_dynamicasset_trigger() { - var result = sut.Trigger("AssetChanged"); + var actual = sut.Trigger("AssetChanged"); - AssertAssetTrigger(result); + AssertAssetTrigger(actual); } - private static void AssertAssetTrigger(IReadOnlyList result) + private static void AssertAssetTrigger(IReadOnlyList actual) { - AssertCompletion(result, + AssertCompletion(actual, PresetActor("event.actor"), PresetUser("event.user"), new[] @@ -235,22 +235,22 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting [Fact] public void Should_describe_comment_trigger() { - var result = sut.CommentTrigger(); + var actual = sut.CommentTrigger(); - AssertCommentTrigger(result); + AssertCommentTrigger(actual); } [Fact] public void Should_describe_dynamic_comment_trigger() { - var result = sut.Trigger("Comment"); + var actual = sut.Trigger("Comment"); - AssertCommentTrigger(result); + AssertCommentTrigger(actual); } - private static void AssertCommentTrigger(IReadOnlyList result) + private static void AssertCommentTrigger(IReadOnlyList actual) { - AssertCompletion(result, + AssertCompletion(actual, PresetActor("event.actor"), PresetUser("event.user"), PresetUser("event.mentionedUser"), @@ -270,22 +270,22 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting [Fact] public void Should_describe_schema_trigger() { - var result = sut.SchemaTrigger(); + var actual = sut.SchemaTrigger(); - AssertSchemaTrigger(result); + AssertSchemaTrigger(actual); } [Fact] public void Should_describe_dynamic_schema_trigger() { - var result = sut.Trigger("SchemaChanged"); + var actual = sut.Trigger("SchemaChanged"); - AssertSchemaTrigger(result); + AssertSchemaTrigger(actual); } - private static void AssertSchemaTrigger(IReadOnlyList result) + private static void AssertSchemaTrigger(IReadOnlyList actual) { - AssertCompletion(result, + AssertCompletion(actual, PresetActor("event.actor"), PresetUser("event.user"), new[] @@ -307,22 +307,22 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting [Fact] public void Should_describe_usage_trigger() { - var result = sut.UsageTrigger(); + var actual = sut.UsageTrigger(); - AssertUsageTrigger(result); + AssertUsageTrigger(actual); } [Fact] public void Should_describe_dynamic_usage_trigger() { - var result = sut.Trigger("Usage"); + var actual = sut.Trigger("Usage"); - AssertUsageTrigger(result); + AssertUsageTrigger(actual); } - private static void AssertUsageTrigger(IReadOnlyList result) + private static void AssertUsageTrigger(IReadOnlyList actual) { - AssertCompletion(result, + AssertCompletion(actual, new[] { "event", @@ -337,11 +337,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting }); } - private static void AssertCompletion(IReadOnlyList result, params string[][] expected) + private static void AssertCompletion(IReadOnlyList actual, params string[][] expected) { var allExpected = expected.SelectMany(x => x).ToArray(); - var paths = result.Select(x => x.Path).ToArray(); + var paths = actual.Select(x => x.Path).ToArray(); foreach (var value in paths) { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageWrapperTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageWrapperTests.cs index 0e478cc42..08cffe61e 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageWrapperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageWrapperTests.cs @@ -41,9 +41,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Subscriptions var sut = new EventMessageWrapper(envelope, new[] { creator1, creator2 }); - var result = await sut.CreatePayloadAsync(); + var actual = await sut.CreatePayloadAsync(); - Assert.Same(enrichedEvent, result); + Assert.Same(enrichedEvent, actual); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs index 68e65f413..873dc25e6 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs @@ -42,9 +42,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates User = RefToken.User("me") }; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Theory] @@ -58,9 +58,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates Id = NamedId.Of("42", "my-app") }; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal(expected, result); + Assert.Equal(expected, actual); } [Fact] @@ -73,9 +73,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates var template = "{{ e.id }}"; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal(value.Id.ToString(), result); + Assert.Equal(value.Id.ToString(), actual); } [Fact] @@ -88,9 +88,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates var template = "{{ e.type }}"; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal(value.Type.ToString(), result); + Assert.Equal(value.Type.ToString(), actual); } [Fact] @@ -105,9 +105,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates var template = "{{ e.timestamp | format_date: 'yyyy-MM-dd-hh-mm-ss' }}"; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal($"{now:yyyy-MM-dd-hh-mm-ss}", result); + Assert.Equal($"{now:yyyy-MM-dd-hh-mm-ss}", actual); } [Fact] @@ -124,9 +124,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates .AddLocalized("en", "Hello")) }; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal("Hello", result); + Assert.Equal("Hello", actual); } [Fact] @@ -139,9 +139,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates Text = "

Hello World

" }; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal("Hello World", result); + Assert.Equal("Hello World", actual); } [Fact] @@ -154,9 +154,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates Text = "## Hello World" }; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal("Hello World", result); + Assert.Equal("Hello World", actual); } [Fact] @@ -169,9 +169,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates Text = "Hello World" }; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal("2", result); + Assert.Equal("2", actual); } [Fact] @@ -184,9 +184,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates text = "Hello World" }; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal("10", result); + Assert.Equal("10", actual); } [Fact] @@ -199,9 +199,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates text = "HelloWorld" }; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal("HelloWorld".ToMD5(), result); + Assert.Equal("HelloWorld".ToMD5(), actual); } [Fact] @@ -214,9 +214,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates text = "HelloWorld" }; - var result = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal("HelloWorld".ToSha256(), result); + Assert.Equal("HelloWorld".ToSha256(), actual); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs index 6ed86065c..1c47cf997 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -31,9 +31,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { yield return new AssetsValidator(assets.Properties.IsRequired, assets.Properties, ids => { - var result = ids.Select(TestAssets.Document).ToList(); + var actual = ids.Select(TestAssets.Document).ToList(); - return Task.FromResult>(result); + return Task.FromResult>(actual); }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs index a8cf97b22..af1560d22 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs @@ -198,7 +198,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static JsonValue CreateValue(int count, string? type, string key, JsonValue value, string? discriminator = null) { - var result = new JsonArray(); + var actual = new JsonArray(); for (var i = 0; i < count; i++) { @@ -213,10 +213,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent obj.Add(key, value); - result.Add(obj); + actual.Add(obj); } - return result; + return actual; } private (DomainId, RootField, ResolvedComponents) Field(ComponentsFieldProperties properties, bool isRequired = false) diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs index 7bebcbc78..0f46c9b6d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -40,9 +40,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { yield return new ReferencesValidator(references.Properties.IsRequired, references.Properties, ids => { - var result = ids.Select(x => new ContentIdStatus(schemaId, x, Status.Published)).ToList(); + var actual = ids.Select(x => new ContentIdStatus(schemaId, x, Status.Published)).ToList(); - return Task.FromResult>(result); + return Task.FromResult>(actual); }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs index b82a3e0e1..32c506e81 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs @@ -261,9 +261,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { return ids => { - var result = new List { Document, Image1, Image2, ImageSvg, Video }; + var actual = new List { Document, Image1, Image2, ImageSvg, Video }; - return Task.FromResult>(result); + return Task.FromResult>(actual); }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs index 1229a7585..93661fc9f 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs @@ -219,9 +219,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { return x => { - var result = references.Select(x => new ContentIdStatus(schemaId, x.Id, x.Status)).ToList(); + var actual = references.Select(x => new ContentIdStatus(schemaId, x.Id, x.Status)).ToList(); - return Task.FromResult>(result); + return Task.FromResult>(actual); }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs index 56be531d2..a44650107 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs @@ -187,26 +187,26 @@ namespace Squidex.Domain.Apps.Core.TestHelpers public static TEvent CreateEvent(Action? init = null) where TEvent : IEvent, new() { - var result = new TEvent(); + var actual = new TEvent(); - if (result is SquidexEvent squidexEvent) + if (actual is SquidexEvent squidexEvent) { - squidexEvent.Actor = new RefToken(RefTokenType.Client, "my-client"); + squidexEvent.Actor = RefToken.Client("my-client"); } - if (result is AppEvent appEvent) + if (actual is AppEvent appEvent) { appEvent.AppId = NamedId.Of(DomainId.NewGuid(), "my-app"); } - if (result is SchemaEvent schemaEvent) + if (actual is SchemaEvent schemaEvent) { schemaEvent.SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); } - init?.Invoke(result); + init?.Invoke(actual); - return result; + return actual; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs index e68bd8a26..8cabce08f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs @@ -13,6 +13,8 @@ using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Indexes; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Domain.Apps.Entities.Teams.Indexes; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Security; @@ -27,6 +29,7 @@ namespace Squidex.Domain.Apps.Entities private readonly IAppsIndex indexForApps = A.Fake(); private readonly IRulesIndex indexForRules = A.Fake(); private readonly ISchemasIndex indexForSchemas = A.Fake(); + private readonly ITeamsIndex indexForTeams = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly NamedId schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); private readonly IAppEntity app; @@ -38,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities app = Mocks.App(appId); - sut = new AppProvider(indexForApps, indexForRules, indexForSchemas, new AsyncLocalCache()); + sut = new AppProvider(indexForApps, indexForRules, indexForSchemas, indexForTeams, new AsyncLocalCache()); } [Fact] @@ -52,9 +55,22 @@ namespace Squidex.Domain.Apps.Entities A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schema.Id, false, ct)) .Returns(schema); - var result = await sut.GetAppWithSchemaAsync(app.Id, schemaId.Id, false, ct); + var actual = await sut.GetAppWithSchemaAsync(app.Id, schemaId.Id, false, ct); - Assert.Equal(schema, result.Item2); + Assert.Equal(schema, actual.Item2); + } + + [Fact] + public async Task Should_get_team_apps_from_index() + { + var team = Mocks.Team(DomainId.NewGuid()); + + A.CallTo(() => indexForApps.GetAppsForTeamAsync(team.Id, ct)) + .Returns(new List { app }); + + var actual = await sut.GetTeamAppsAsync(team.Id, ct); + + Assert.Equal(app, actual.Single()); } [Fact] @@ -65,9 +81,9 @@ namespace Squidex.Domain.Apps.Entities A.CallTo(() => indexForApps.GetAppsForUserAsync("user1", permissions, ct)) .Returns(new List { app }); - var result = await sut.GetUserAppsAsync("user1", permissions, ct); + var actual = await sut.GetUserAppsAsync("user1", permissions, ct); - Assert.Equal(app, result.Single()); + Assert.Equal(app, actual.Single()); } [Fact] @@ -76,9 +92,9 @@ namespace Squidex.Domain.Apps.Entities A.CallTo(() => indexForApps.GetAppAsync(app.Id, false, ct)) .Returns(app); - var result = await sut.GetAppAsync(app.Id, false, ct); + var actual = await sut.GetAppAsync(app.Id, false, ct); - Assert.Equal(app, result); + Assert.Equal(app, actual); } [Fact] @@ -87,9 +103,35 @@ namespace Squidex.Domain.Apps.Entities A.CallTo(() => indexForApps.GetAppAsync(app.Name, false, ct)) .Returns(app); - var result = await sut.GetAppAsync(app.Name, false, ct); + var actual = await sut.GetAppAsync(app.Name, false, ct); + + Assert.Equal(app, actual); + } + + [Fact] + public async Task Should_get_team_from_index() + { + var team = Mocks.Team(DomainId.NewGuid()); + + A.CallTo(() => indexForTeams.GetTeamAsync(team.Id, ct)) + .Returns(team); + + var actual = await sut.GetTeamAsync(team.Id, ct); + + Assert.Equal(team, actual); + } + + [Fact] + public async Task Should_get_teams_from_index() + { + var team = Mocks.Team(DomainId.NewGuid()); + + A.CallTo(() => indexForTeams.GetTeamsAsync("user1", ct)) + .Returns(new List { team }); + + var actual = await sut.GetUserTeamsAsync("user1", ct); - Assert.Equal(app, result); + Assert.Equal(team, actual.Single()); } [Fact] @@ -100,9 +142,9 @@ namespace Squidex.Domain.Apps.Entities A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schema.Id, false, ct)) .Returns(schema); - var result = await sut.GetSchemaAsync(app.Id, schema.Id, false, ct); + var actual = await sut.GetSchemaAsync(app.Id, schema.Id, false, ct); - Assert.Equal(schema, result); + Assert.Equal(schema, actual); } [Fact] @@ -113,9 +155,9 @@ namespace Squidex.Domain.Apps.Entities A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schemaId.Name, false, ct)) .Returns(schema); - var result = await sut.GetSchemaAsync(app.Id, schemaId.Name, false, ct); + var actual = await sut.GetSchemaAsync(app.Id, schemaId.Name, false, ct); - Assert.Equal(schema, result); + Assert.Equal(schema, actual); } [Fact] @@ -126,9 +168,9 @@ namespace Squidex.Domain.Apps.Entities A.CallTo(() => indexForSchemas.GetSchemasAsync(app.Id, ct)) .Returns(new List { schema }); - var result = await sut.GetSchemasAsync(app.Id, ct); + var actual = await sut.GetSchemasAsync(app.Id, ct); - Assert.Equal(schema, result.Single()); + Assert.Equal(schema, actual.Single()); } [Fact] @@ -139,9 +181,9 @@ namespace Squidex.Domain.Apps.Entities A.CallTo(() => indexForRules.GetRulesAsync(app.Id, ct)) .Returns(new List { rule }); - var result = await sut.GetRulesAsync(app.Id, ct); + var actual = await sut.GetRulesAsync(app.Id, ct); - Assert.Equal(rule, result.Single()); + Assert.Equal(rule, actual.Single()); } [Fact] @@ -152,9 +194,9 @@ namespace Squidex.Domain.Apps.Entities A.CallTo(() => indexForRules.GetRulesAsync(app.Id, ct)) .Returns(new List { rule }); - var result = await sut.GetRuleAsync(app.Id, rule.Id, ct); + var actual = await sut.GetRuleAsync(app.Id, rule.Id, ct); - Assert.Equal(rule, result); + Assert.Equal(rule, actual); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs index 3390c29a7..66f3bb30f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs @@ -34,13 +34,13 @@ namespace Squidex.Domain.Apps.Entities.Apps { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("xyz", ctx, default); + var actual = await sut.SearchAsync("xyz", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_return_dashboard_result_if_matching_and_permission_given() + public async Task Should_return_dashboard_actual_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppUsage, appId.Name); @@ -49,25 +49,25 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => urlGenerator.DashboardUI(appId)) .Returns("dashboard-url"); - var result = await sut.SearchAsync("dashboard", ctx, default); + var actual = await sut.SearchAsync("dashboard", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Dashboard", SearchResultType.Dashboard, "dashboard-url")); } [Fact] - public async Task Should_not_return_dashboard_result_if_user_has_no_permission() + public async Task Should_not_return_dashboard_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("assets", ctx, default); + var actual = await sut.SearchAsync("assets", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_return_languages_result_if_matching_and_permission_given() + public async Task Should_return_languages_actual_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppLanguagesRead, appId.Name); @@ -76,35 +76,35 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => urlGenerator.LanguagesUI(appId)) .Returns("languages-url"); - var result = await sut.SearchAsync("languages", ctx, default); + var actual = await sut.SearchAsync("languages", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Languages", SearchResultType.Setting, "languages-url")); } [Fact] - public async Task Should_not_return_languages_result_if_user_has_no_permission() + public async Task Should_not_return_languages_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("assets", ctx, default); + var actual = await sut.SearchAsync("assets", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_not_return_patterns_result_if_user_has_no_permission() + public async Task Should_not_return_patterns_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("patterns", ctx, default); + var actual = await sut.SearchAsync("patterns", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_return_schemas_result_if_matching_and_permission_given() + public async Task Should_return_schemas_actual_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppSchemasRead, appId.Name); @@ -113,25 +113,25 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => urlGenerator.SchemasUI(appId)) .Returns("schemas-url"); - var result = await sut.SearchAsync("schemas", ctx, default); + var actual = await sut.SearchAsync("schemas", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Schemas", SearchResultType.Schema, "schemas-url")); } [Fact] - public async Task Should_not_return_schemas_result_if_user_has_no_permission() + public async Task Should_not_return_schemas_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("schemas", ctx, default); + var actual = await sut.SearchAsync("schemas", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_return_assets_result_if_matching_and_permission_given() + public async Task Should_return_assets_actual_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, appId.Name); @@ -140,25 +140,25 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => urlGenerator.AssetsUI(appId, A._)) .Returns("assets-url"); - var result = await sut.SearchAsync("assets", ctx, default); + var actual = await sut.SearchAsync("assets", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Assets", SearchResultType.Asset, "assets-url")); } [Fact] - public async Task Should_not_return_assets_result_if_user_has_no_permission() + public async Task Should_not_return_assets_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("assets", ctx, default); + var actual = await sut.SearchAsync("assets", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_return_backups_result_if_matching_and_permission_given() + public async Task Should_return_backups_actual_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppBackupsRead, appId.Name); @@ -167,25 +167,25 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => urlGenerator.BackupsUI(appId)) .Returns("backups-url"); - var result = await sut.SearchAsync("backups", ctx, default); + var actual = await sut.SearchAsync("backups", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Backups", SearchResultType.Setting, "backups-url")); } [Fact] - public async Task Should_not_return_backups_result_if_user_has_no_permission() + public async Task Should_not_return_backups_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("backups", ctx, default); + var actual = await sut.SearchAsync("backups", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_return_clients_result_if_matching_and_permission_given() + public async Task Should_return_clients_actual_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppClientsRead, appId.Name); @@ -194,25 +194,25 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => urlGenerator.ClientsUI(appId)) .Returns("clients-url"); - var result = await sut.SearchAsync("clients", ctx, default); + var actual = await sut.SearchAsync("clients", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Clients", SearchResultType.Setting, "clients-url")); } [Fact] - public async Task Should_not_return_clients_result_if_user_has_no_permission() + public async Task Should_not_return_clients_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("clients", ctx, default); + var actual = await sut.SearchAsync("clients", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_return_contributors_result_if_matching_and_permission_given() + public async Task Should_return_contributors_actual_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppContributorsRead, appId.Name); @@ -221,25 +221,25 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => urlGenerator.ContributorsUI(appId)) .Returns("contributors-url"); - var result = await sut.SearchAsync("contributors", ctx, default); + var actual = await sut.SearchAsync("contributors", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Contributors", SearchResultType.Setting, "contributors-url")); } [Fact] - public async Task Should_not_contributors_clients_result_if_user_has_no_permission() + public async Task Should_not_contributors_clients_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("contributors", ctx, default); + var actual = await sut.SearchAsync("contributors", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_return_subscription_result_if_matching_and_permission_given() + public async Task Should_return_subscription_actual_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppPlansRead, appId.Name); @@ -248,25 +248,25 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => urlGenerator.PlansUI(appId)) .Returns("subscription-url"); - var result = await sut.SearchAsync("subscription", ctx, default); + var actual = await sut.SearchAsync("subscription", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Subscription", SearchResultType.Setting, "subscription-url")); } [Fact] - public async Task Should_not_subscription_clients_result_if_user_has_no_permission() + public async Task Should_not_subscription_clients_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("subscription", ctx, default); + var actual = await sut.SearchAsync("subscription", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_return_roles_result_if_matching_and_permission_given() + public async Task Should_return_roles_actual_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppRolesRead, appId.Name); @@ -275,25 +275,25 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => urlGenerator.RolesUI(appId)) .Returns("roles-url"); - var result = await sut.SearchAsync("roles", ctx, default); + var actual = await sut.SearchAsync("roles", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Roles", SearchResultType.Setting, "roles-url")); } [Fact] - public async Task Should_not_roles_clients_result_if_user_has_no_permission() + public async Task Should_not_roles_clients_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("roles", ctx, default); + var actual = await sut.SearchAsync("roles", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_return_rules_result_if_matching_and_permission_given() + public async Task Should_return_rules_actual_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppRulesRead, appId.Name); @@ -302,25 +302,25 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => urlGenerator.RulesUI(appId)) .Returns("rules-url"); - var result = await sut.SearchAsync("rules", ctx, default); + var actual = await sut.SearchAsync("rules", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Rules", SearchResultType.Rule, "rules-url")); } [Fact] - public async Task Should_not_return_rules_result_if_user_has_no_permission() + public async Task Should_not_return_rules_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("assets", ctx, default); + var actual = await sut.SearchAsync("assets", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] - public async Task Should_return_workflows_result_if_matching_and_permission_given() + public async Task Should_return_workflows_actual_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppWorkflowsRead, appId.Name); @@ -329,21 +329,21 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => urlGenerator.WorkflowsUI(appId)) .Returns("workflows-url"); - var result = await sut.SearchAsync("workflows", ctx, default); + var actual = await sut.SearchAsync("workflows", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Workflows", SearchResultType.Setting, "workflows-url")); } [Fact] - public async Task Should_not_return_workflows_result_if_user_has_no_permission() + public async Task Should_not_return_workflows_actual_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("workflows", ctx, default); + var actual = await sut.SearchAsync("workflows", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); } private Context ContextWithPermission(string? permission = null) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs index 9859f5ba6..ef9ab078c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using FakeItEasy; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; @@ -58,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var app = Mocks.App(NamedId.Of(appId, "my-app")); A.CallTo(() => app.Contributors) - .Returns(AppContributors.Empty.Assign(userId, Role.Owner)); + .Returns(Contributors.Empty.Assign(userId, Role.Owner)); var rootState = new TestState(appId, state.PersistenceFactory); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs index 94ebef50c..4b99d3ee2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs @@ -178,9 +178,9 @@ namespace Squidex.Domain.Apps.Entities.Apps ContributorId = "found" }); - var result = await sut.RestoreEventAsync(@event, context, ct); + var actual = await sut.RestoreEventAsync(@event, context, ct); - Assert.True(result); + Assert.True(actual); Assert.Equal("found_mapped", @event.Payload.ContributorId); } @@ -194,9 +194,9 @@ namespace Squidex.Domain.Apps.Entities.Apps ContributorId = "unknown" }); - var result = await sut.RestoreEventAsync(@event, context, ct); + var actual = await sut.RestoreEventAsync(@event, context, ct); - Assert.False(result); + Assert.False(actual); Assert.Equal("unknown", @event.Payload.ContributorId); } @@ -210,9 +210,9 @@ namespace Squidex.Domain.Apps.Entities.Apps ContributorId = "found" }); - var result = await sut.RestoreEventAsync(@event, context, ct); + var actual = await sut.RestoreEventAsync(@event, context, ct); - Assert.True(result); + Assert.True(actual); Assert.Equal("found_mapped", @event.Payload.ContributorId); } @@ -226,9 +226,9 @@ namespace Squidex.Domain.Apps.Entities.Apps ContributorId = "unknown" }); - var result = await sut.RestoreEventAsync(@event, context, ct); + var actual = await sut.RestoreEventAsync(@event, context, ct); - Assert.False(result); + Assert.False(actual); Assert.Equal("unknown", @event.Payload.ContributorId); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs index 4c07081fb..4d1ec842f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs @@ -46,13 +46,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject } [Fact] - public async Task Should_replace_context_app_with_domain_object_result() + public async Task Should_replace_context_app_with_domain_object_actual() { - var result = A.Fake(); + var actual = A.Fake(); - await HandleAsync(new UpdateApp(), result); + await HandleAsync(new UpdateApp(), actual); - Assert.Same(result, requestContext.App); + Assert.Same(actual, requestContext.App); } [Fact] @@ -82,14 +82,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await Assert.ThrowsAsync(() => HandleAsync(sut, command)); } - private Task HandleAsync(AppUpdateCommand command, object result) + private Task HandleAsync(AppCommand command, object actual) { command.AppId = appId; var domainObject = A.Fake(); A.CallTo(() => domainObject.ExecuteAsync(A._, A._)) - .Returns(new CommandResult(command.AggregateId, 1, 0, result)); + .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); A.CallTo(() => domainObjectFactory.Create(command.AggregateId)) .Returns(domainObject); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs index dccd42af9..8ddadd13e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs @@ -12,10 +12,11 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Json.Objects; using Squidex.Shared.Users; using Xunit; @@ -24,8 +25,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { public class AppDomainObjectTests : HandlerTestBase { - private readonly IAppPlansProvider appPlansProvider = A.Fake(); - private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly IBillingPlans billingPlans = A.Fake(); + private readonly IBillingManager billingManager = A.Fake(); private readonly IUser user; private readonly IUserResolver userResolver = A.Fake(); private readonly string contributorId = DomainId.NewGuid().ToString(); @@ -34,9 +36,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject private readonly string roleName = "My Role"; private readonly string planIdPaid = "premium"; private readonly string planIdFree = "free"; - private readonly AppDomainObject sut; - private readonly DomainId workflowId = DomainId.NewGuid(); private readonly InitialSettings initialSettings; + private readonly DomainId teamId = DomainId.NewGuid(); + private readonly DomainId workflowId = DomainId.NewGuid(); + private readonly AppDomainObject sut; protected override DomainId Id { @@ -50,18 +53,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId, default)) .Returns(user); - A.CallTo(() => appPlansProvider.GetFreePlan()) - .Returns(new ConfigAppLimitsPlan { Id = planIdFree, MaxContributors = 10 }); + A.CallTo(() => billingPlans.GetFreePlan()) + .Returns(new Plan { Id = planIdFree, MaxContributors = 10 }); - A.CallTo(() => appPlansProvider.GetPlan(planIdFree)) - .Returns(new ConfigAppLimitsPlan { Id = planIdFree, MaxContributors = 10 }); + A.CallTo(() => billingPlans.GetPlan(planIdFree)) + .Returns(new Plan { Id = planIdFree, MaxContributors = 10 }); - A.CallTo(() => appPlansProvider.GetPlan(planIdPaid)) - .Returns(new ConfigAppLimitsPlan { Id = planIdPaid, MaxContributors = 30 }); + A.CallTo(() => billingPlans.GetPlan(planIdPaid)) + .Returns(new Plan { Id = planIdPaid, MaxContributors = 30 }); - A.CallTo(() => appPlansBillingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, A._, default)) + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, A._, default)) .Returns(Task.FromResult(null)); + A.CallTo(() => appProvider.GetTeamAsync(teamId, default)) + .Returns(Mocks.Team(teamId, contributor: Actor.Identifier)); + // Create a non-empty setting, otherwise the event is not raised as it does not change the domain object. initialSettings = new InitialSettings { @@ -73,9 +79,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject var serviceProvider = new ServiceCollection() + .AddSingleton(appProvider) .AddSingleton(initialSettings) - .AddSingleton(appPlansProvider) - .AddSingleton(appPlansBillingManager) + .AddSingleton(billingPlans) + .AddSingleton(billingManager) .AddSingleton(userResolver) .BuildServiceProvider(); @@ -100,9 +107,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { var command = new CreateApp { Name = AppName, AppId = AppId }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(AppName, sut.Snapshot.Name); @@ -119,9 +126,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { var command = new CreateApp { Name = AppName, Actor = ActorClient, AppId = AppId }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(AppName, sut.Snapshot.Name); @@ -139,12 +146,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal("my-label", sut.Snapshot.Label); - Assert.Equal("my-description", sut.Snapshot.Description); + Assert.Equal(command.Label, sut.Snapshot.Label); + Assert.Equal(command.Description, sut.Snapshot.Description); LastEvents .ShouldHaveSameEvents( @@ -164,9 +171,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(settings, sut.Snapshot.Settings); @@ -183,11 +190,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal("image/png", sut.Snapshot.Image!.MimeType); + Assert.Equal(command.File.MimeType, sut.Snapshot.Image!.MimeType); LastEvents .ShouldHaveSameEvents( @@ -203,9 +210,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteUploadImage(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Null(sut.Snapshot.Image); @@ -220,14 +227,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { var command = new ChangePlan { PlanId = planIdPaid }; - A.CallTo(() => appPlansBillingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, planIdPaid, default)) + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, planIdPaid, default)) .Returns(Task.FromResult(null)); await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); @@ -236,10 +243,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject CreateEvent(new AppPlanChanged { PlanId = planIdPaid }) ); - A.CallTo(() => appPlansBillingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, planIdPaid, default)) + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, planIdPaid, default)) .MustHaveHappened(); - A.CallTo(() => appPlansBillingManager.SubscribeAsync(Actor.Identifier, AppNamedId, planIdPaid, default)) + A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, AppNamedId, planIdPaid, default)) .MustHaveHappened(); } @@ -250,9 +257,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); @@ -261,10 +268,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject CreateEvent(new AppPlanChanged { PlanId = planIdPaid }) ); - A.CallTo(() => appPlansBillingManager.MustRedirectToPortalAsync(A._, A>._, A._, A._)) + A.CallTo(() => billingManager.MustRedirectToPortalAsync(A._, A>._, A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => appPlansBillingManager.SubscribeAsync(A._, A>._, A._, A._)) + A.CallTo(() => billingManager.SubscribeAsync(A._, A>._, A._, A._)) .MustNotHaveHappened(); } @@ -276,9 +283,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteChangePlanAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new PlanChangedResult(planIdFree, true)); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdFree, true)); Assert.Null(sut.Snapshot.Plan); @@ -287,10 +294,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject CreateEvent(new AppPlanReset()) ); - A.CallTo(() => appPlansBillingManager.MustRedirectToPortalAsync(A._, A>._, A._, A._)) + A.CallTo(() => billingManager.MustRedirectToPortalAsync(A._, A>._, A._, A._)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => appPlansBillingManager.UnsubscribeAsync(A._, A>._, A._)) + A.CallTo(() => billingManager.UnsubscribeAsync(A._, A>._, A._)) .MustNotHaveHappened(); } @@ -302,9 +309,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteChangePlanAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new PlanChangedResult(planIdFree, true)); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdFree, true)); Assert.Null(sut.Snapshot.Plan); @@ -313,26 +320,26 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject CreateEvent(new AppPlanReset()) ); - A.CallTo(() => appPlansBillingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, planIdPaid, default)) + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, planIdPaid, default)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => appPlansBillingManager.UnsubscribeAsync(A._, A>._, A._)) + A.CallTo(() => billingManager.UnsubscribeAsync(A._, A>._, A._)) .MustHaveHappened(); } [Fact] - public async Task ChangePlan_should_not_make_update_for_redirect_result() + public async Task ChangePlan_should_not_make_update_for_redirect_actual() { var command = new ChangePlan { PlanId = planIdPaid }; - A.CallTo(() => appPlansBillingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, planIdPaid, default)) + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, planIdPaid, default)) .Returns(new Uri("http://squidex.io")); await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new PlanChangedResult(planIdPaid, false, new Uri("http://squidex.io"))); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid, false, new Uri("http://squidex.io"))); Assert.Null(sut.Snapshot.Plan); } @@ -344,16 +351,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); Assert.Equal(planIdPaid, sut.Snapshot.Plan?.PlanId); - A.CallTo(() => appPlansBillingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, planIdPaid, A._)) + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, AppNamedId, planIdPaid, A._)) .MustNotHaveHappened(); - A.CallTo(() => appPlansBillingManager.SubscribeAsync(Actor.Identifier, AppNamedId, planIdPaid, A._)) + A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, AppNamedId, planIdPaid, A._)) .MustNotHaveHappened(); } @@ -364,9 +371,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]); @@ -384,9 +391,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteAssignContributorAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]); @@ -404,9 +411,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteAssignContributorAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); @@ -416,6 +423,45 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject ); } + [Fact] + public async Task Transfer_should_create_events_and_set_team() + { + var command = new TransferToTeam { TeamId = teamId }; + + await ExecuteCreateAsync(); + + var actual = await PublishAsync(command); + + actual.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(teamId, sut.Snapshot.TeamId); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppTransfered { TeamId = teamId }) + ); + } + + [Fact] + public async Task Transfer_from_team_should_create_events_and_set_team() + { + var command = new TransferToTeam { TeamId = null }; + + await ExecuteCreateAsync(); + await ExecuteTransferAsync(); + + var actual = await PublishAsync(command); + + actual.ShouldBeEquivalent(sut.Snapshot); + + Assert.Null(sut.Snapshot.TeamId); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppTransfered { TeamId = null }) + ); + } + [Fact] public async Task AttachClient_should_create_events_and_add_client() { @@ -423,9 +469,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); @@ -443,9 +489,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteAttachClientAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); @@ -463,9 +509,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteAttachClientAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); @@ -482,9 +528,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.NotEmpty(sut.Snapshot.Workflows); @@ -502,9 +548,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteAddWorkflowAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.NotEmpty(sut.Snapshot.Workflows); @@ -522,9 +568,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteAddWorkflowAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Empty(sut.Snapshot.Workflows); @@ -541,9 +587,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.Languages.Contains(Language.DE)); @@ -561,9 +607,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteAddLanguageAsync(Language.DE); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.Languages.Contains(Language.DE)); @@ -581,9 +627,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteAddLanguageAsync(Language.DE); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.Languages.Contains(Language.DE)); @@ -600,9 +646,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(1, sut.Snapshot.Roles.CustomCount); @@ -620,9 +666,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteAddRoleAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(0, sut.Snapshot.Roles.CustomCount); @@ -640,9 +686,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); await ExecuteAddRoleAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -657,9 +703,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); Assert.True(sut.Snapshot.IsDeleted); @@ -668,7 +714,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject CreateEvent(new AppDeleted()) ); - A.CallTo(() => appPlansBillingManager.UnsubscribeAsync(command.Actor.Identifier, AppNamedId, default)) + A.CallTo(() => billingManager.UnsubscribeAsync(command.Actor.Identifier, AppNamedId, default)) .MustHaveHappened(); } @@ -712,21 +758,26 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return PublishAsync(new ChangePlan { PlanId = planIdPaid }); } + private Task ExecuteTransferAsync() + { + return PublishAsync(new TransferToTeam { TeamId = teamId }); + } + private Task ExecuteArchiveAsync() { return PublishAsync(new DeleteApp()); } - private Task PublishIdempotentAsync(AppCommand command) + private Task PublishIdempotentAsync(T command) where T : SquidexCommand, IAggregateCommand { return PublishIdempotentAsync(sut, CreateCommand(command)); } - private async Task PublishAsync(AppCommand command) + private async Task PublishAsync(T command) where T : SquidexCommand, IAggregateCommand { - var result = await sut.ExecuteAsync(CreateCommand(command), default); + var actual = await sut.ExecuteAsync(CreateCommand(command), default); - return result.Payload; + return actual.Payload; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppState.json b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppState.json index d8cc06e8d..5a998bbd4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppState.json +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppState.json @@ -2,6 +2,9 @@ "name": "test", "label": "label", "description": "description", + "contributors": { + "62cee33bcc71c0d140ad103a": "Owner" + }, "roles": { "custom": { "permissions": [ @@ -47,9 +50,6 @@ "hideScheduler": false, "hideDateTimeModeButton": false }, - "contributors": { - "62cee33bcc71c0d140ad103a": "Owner" - }, "assetScripts": { }, diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs index aa87cff66..161eb8ea1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs @@ -6,10 +6,11 @@ // ========================================================================== using FakeItEasy; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Validation; @@ -26,8 +27,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards private readonly IUser user2 = UserMocks.User("2"); private readonly IUser user3 = UserMocks.User("3"); private readonly IUserResolver users = A.Fake(); - private readonly IAppLimitsPlan appPlan = A.Fake(); - private readonly AppContributors contributors_0 = AppContributors.Empty; + private readonly Contributors contributors_0 = Contributors.Empty; + private readonly Plan planWithoutLimit = new Plan { MaxContributors = -1 }; + private readonly Plan planWithLimit = new Plan { MaxContributors = 2 }; private readonly Roles roles = Roles.Empty; public GuardAppContributorsTests() @@ -52,9 +54,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards A.CallTo(() => users.FindByIdAsync("notfound", default)) .Returns(Task.FromResult(null)); - - A.CallTo(() => appPlan.MaxContributors) - .Returns(10); } [Fact] @@ -62,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var command = new AssignContributor(); - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, appPlan), + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit), new ValidationError("Contributor ID or email is required.", "ContributorId")); } @@ -71,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var command = new AssignContributor { ContributorId = "1", Role = "Invalid" }; - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, appPlan), + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit), new ValidationError("Role is not a valid value.", "Role")); } @@ -82,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards var contributors_1 = contributors_0.Assign("1", Role.Owner); - await GuardAppContributors.CanAssign(command, App(contributors_1), users, appPlan); + await GuardAppContributors.CanAssign(command, App(contributors_1), users, planWithoutLimit); } [Fact] @@ -92,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards var contributors_1 = contributors_0.Assign("1", Role.Owner); - await GuardAppContributors.CanAssign(command, App(contributors_1), users, appPlan); + await GuardAppContributors.CanAssign(command, App(contributors_1), users, planWithoutLimit); } [Fact] @@ -100,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner }; - await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, appPlan)); + await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit)); } [Fact] @@ -108,85 +107,70 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = RefToken.User("3") }; - await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, appPlan)); + await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit)); } [Fact] public async Task CanAssign_should_throw_exception_if_contributor_max_reached() { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - var command = new AssignContributor { ContributorId = "3" }; var contributors_1 = contributors_0.Assign("1", Role.Owner); var contributors_2 = contributors_1.Assign("2", Role.Editor); - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_2), users, appPlan), + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithLimit), new ValidationError("You have reached the maximum number of contributors for your plan.")); } [Fact] public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_ignored() { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - var command = new AssignContributor { ContributorId = "3", IgnorePlans = true }; var contributors_1 = contributors_0.Assign("1", Role.Owner); var contributors_2 = contributors_1.Assign("2", Role.Editor); - await GuardAppContributors.CanAssign(command, App(contributors_2), users, appPlan); + await GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithLimit); } [Fact] public async Task CanAssign_should_not_throw_exception_if_user_found() { - A.CallTo(() => appPlan.MaxContributors) - .Returns(-1); - var command = new AssignContributor { ContributorId = "1" }; - await GuardAppContributors.CanAssign(command, App(contributors_0), users, appPlan); + await GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit); } [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_has_another_role() + public async Task CanAssign_should_not_throw_exception_if_role_is_valid() { var command = new AssignContributor { ContributorId = "1" }; var contributors_1 = contributors_0.Assign("1", Role.Developer); - await GuardAppContributors.CanAssign(command, App(contributors_1), users, appPlan); + await GuardAppContributors.CanAssign(command, App(contributors_1), users, planWithoutLimit); } [Fact] public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_role_changed() { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - var command = new AssignContributor { ContributorId = "1" }; var contributors_1 = contributors_0.Assign("1", Role.Developer); var contributors_2 = contributors_1.Assign("2", Role.Developer); - await GuardAppContributors.CanAssign(command, App(contributors_2), users, appPlan); + await GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithLimit); } [Fact] public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_ígnored() { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - var command = new AssignContributor { ContributorId = "3", IgnorePlans = true }; var contributors_1 = contributors_0.Assign("1", Role.Editor); var contributors_2 = contributors_1.Assign("2", Role.Editor); - await GuardAppContributors.CanAssign(command, App(contributors_2), users, appPlan); + await GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithoutLimit); } [Fact] @@ -229,7 +213,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards GuardAppContributors.CanRemove(command, App(contributors_2)); } - private IAppEntity App(AppContributors contributors) + private IAppEntity App(Contributors contributors) { var app = A.Fake(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs index 800add091..1de0c2453 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using FakeItEasy; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Apps.Commands; @@ -23,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards private readonly string roleName = "Role1"; private readonly Roles roles_0 = Roles.Empty; private readonly AppClients clients = AppClients.Empty.Add("client", "secret", "clientRole"); - private readonly AppContributors contributors = AppContributors.Empty.Assign("contributor", "contributorRole"); + private readonly Contributors contributors = Contributors.Empty.Assign("contributor", "contributorRole"); [Fact] public void CanAdd_should_throw_exception_if_name_empty() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs index c9cb43d17..7e151e445 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs @@ -6,10 +6,12 @@ // ========================================================================== using FakeItEasy; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; +using Squidex.Domain.Apps.Entities.Teams; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; @@ -21,24 +23,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { public class GuardAppTests : IClassFixture { + private readonly IAppProvider appProvider = A.Fake(); private readonly IUserResolver users = A.Fake(); - private readonly IAppPlansProvider appPlans = A.Fake(); - private readonly IAppLimitsPlan basicPlan = A.Fake(); - private readonly IAppLimitsPlan freePlan = A.Fake(); + private readonly IBillingPlans billingPlans = A.Fake(); + private readonly RefToken actor = RefToken.User("42"); public GuardAppTests() { A.CallTo(() => users.FindByIdOrEmailAsync(A._, default)) .Returns(A.Dummy()); - A.CallTo(() => appPlans.GetPlan("notfound")) + A.CallTo(() => billingPlans.GetPlan("notfound")) .Returns(null!); - A.CallTo(() => appPlans.GetPlan("basic")) - .Returns(basicPlan); - - A.CallTo(() => appPlans.GetPlan("free")) - .Returns(freePlan); + A.CallTo(() => billingPlans.GetPlan("basic")) + .Returns(new Plan()); } [Fact] @@ -80,9 +79,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var command = new ChangePlan { Actor = RefToken.User("me") }; - AppPlan? plan = null; + AssignedPlan? plan = null; - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), appPlans), + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), billingPlans), new ValidationError("Plan ID is required.", "PlanId")); } @@ -91,9 +90,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var command = new ChangePlan { PlanId = "notfound", Actor = RefToken.User("me") }; - AppPlan? plan = null; + AssignedPlan? plan = null; - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), appPlans), + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), billingPlans), new ValidationError("A plan with this id does not exist.", "PlanId")); } @@ -102,20 +101,31 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; - var plan = new AppPlan(RefToken.User("other"), "premium"); + var plan = new AssignedPlan(RefToken.User("other"), "premium"); - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), appPlans), + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), billingPlans), new ValidationError("Plan can only changed from the user who configured the plan initially.")); } + [Fact] + public void CanChangePlan_should_throw_exception_if_assigned_to_team() + { + var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; + + var teamId = DomainId.NewGuid(); + + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(null, teamId), billingPlans), + new ValidationError("Plan is managed by the team.")); + } + [Fact] public void CanChangePlan_should_not_throw_exception_if_plan_is_the_same() { var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; - var plan = new AppPlan(command.Actor, "basic"); + var plan = new AssignedPlan(command.Actor, "basic"); - GuardApp.CanChangePlan(command, App(plan), appPlans); + GuardApp.CanChangePlan(command, App(plan), billingPlans); } [Fact] @@ -123,9 +133,66 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; - var plan = new AppPlan(command.Actor, "premium"); + var plan = new AssignedPlan(command.Actor, "premium"); + + GuardApp.CanChangePlan(command, App(plan), billingPlans); + } + + [Fact] + public async Task CanTransfer_should_not_throw_exception_if_team_exists() + { + var team = Mocks.Team(DomainId.NewGuid(), contributor: actor.Identifier); + + A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) + .Returns(team); + + var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; + + await GuardApp.CanTransfer(command, App(null), appProvider, default); + } + + [Fact] + public async Task CanTransfer_should_throw_exception_if_team_does_not_exist() + { + var team = Mocks.Team(DomainId.NewGuid(), contributor: actor.Identifier); + + A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) + .Returns(Task.FromResult(null)); + + var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; + + await ValidationAssert.ThrowsAsync(() => GuardApp.CanTransfer(command, App(null), appProvider, default), + new ValidationError("The team does not exist.")); + } + + [Fact] + public async Task CanTransfer_should_throw_exception_if_actor_is_not_part_of_team() + { + var team = Mocks.Team(DomainId.NewGuid()); + + A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) + .Returns(team); + + var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; - GuardApp.CanChangePlan(command, App(plan), appPlans); + await ValidationAssert.ThrowsAsync(() => GuardApp.CanTransfer(command, App(null), appProvider, default), + new ValidationError("The team does not exist.")); + } + + [Fact] + public async Task CanTransfer_should_throw_exception_if_app_has_plan() + { + var team = Mocks.Team(DomainId.NewGuid(), contributor: actor.Identifier); + + A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) + .Returns(team); + + var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; + + var plan = new AssignedPlan(RefToken.User("me"), "premium"); + + await ValidationAssert.ThrowsAsync(() => GuardApp.CanTransfer(command, App(plan), appProvider, default), + new ValidationError("Subscription must be cancelled first before the app can be transfered.")); } [Fact] @@ -248,11 +315,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards GuardApp.CanUpdateSettings(command); } - private static IAppEntity App(AppPlan? plan) + private static IAppEntity App(AssignedPlan? plan, DomainId? teamId = null) { var app = A.Fake(); - A.CallTo(() => app.Plan).Returns(plan); + A.CallTo(() => app.Plan) + .Returns(plan); + + A.CallTo(() => app.TeamId) + .Returns(teamId); return app; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs index 5f7200593..c2135600b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs @@ -149,6 +149,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes Assert.Same(expected, actual[0]); } + [Fact] + public async Task Should_resolve_all_apps_from_team() + { + var teamId = DomainId.NewGuid(); + + var expected = CreateApp(); + + A.CallTo(() => appRepository.QueryAllAsync(teamId, ct)) + .Returns(new List { expected }); + + var actual = await sut.GetAppsForTeamAsync(teamId, ct); + + Assert.Same(expected, actual[0]); + } + [Fact] public async Task Should_return_empty_apps_if_app_not_created() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEventConsumerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEventConsumerTests.cs deleted file mode 100644 index 44b4798ca..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEventConsumerTests.cs +++ /dev/null @@ -1,193 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Microsoft.Extensions.Logging; -using NodaTime; -using Squidex.Domain.Apps.Core.TestHelpers; -using Squidex.Domain.Apps.Entities.Notifications; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Shared.Users; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Invitation -{ - public class InvitationEventConsumerTests - { - private readonly INotificationSender notificatíonSender = A.Fake(); - private readonly IUserResolver userResolver = A.Fake(); - private readonly IUser assigner = UserMocks.User("1"); - private readonly IUser assignee = UserMocks.User("2"); - private readonly ILogger log = A.Fake>(); - private readonly string assignerId = DomainId.NewGuid().ToString(); - private readonly string assigneeId = DomainId.NewGuid().ToString(); - private readonly string appName = "my-app"; - private readonly InvitationEventConsumer sut; - - public InvitationEventConsumerTests() - { - A.CallTo(() => notificatíonSender.IsActive) - .Returns(true); - - A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) - .Returns(assigner); - - A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) - .Returns(assignee); - - sut = new InvitationEventConsumer(notificatíonSender, userResolver, log); - } - - [Fact] - public async Task Should_not_send_email_if_contributors_assigned_by_clients() - { - var @event = CreateEvent(RefTokenType.Client, true); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_for_initial_owner() - { - var @event = CreateEvent(RefTokenType.Subject, false, streamNumber: 1); - - await sut.On(@event); - - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_for_old_events() - { - var created = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50)); - - var @event = CreateEvent(RefTokenType.Subject, true, instant: created); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_for_old_contributor() - { - var @event = CreateEvent(RefTokenType.Subject, true, isNewContributor: false); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_if_sender_not_active() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - A.CallTo(() => notificatíonSender.IsActive) - .Returns(false); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_if_assigner_not_found() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) - .Returns(Task.FromResult(null)); - - await sut.On(@event); - - MustNotSendEmail(); - MustLogWarning(); - } - - [Fact] - public async Task Should_not_send_email_if_assignee_not_found() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) - .Returns(Task.FromResult(null)); - - await sut.On(@event); - - MustNotSendEmail(); - MustLogWarning(); - } - - [Fact] - public async Task Should_send_email_for_new_user() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - await sut.On(@event); - - A.CallTo(() => notificatíonSender.SendInviteAsync(assigner, assignee, appName)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_send_email_for_existing_user() - { - var @event = CreateEvent(RefTokenType.Subject, false); - - await sut.On(@event); - - A.CallTo(() => notificatíonSender.SendInviteAsync(assigner, assignee, appName)) - .MustHaveHappened(); - } - - private void MustLogWarning() - { - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument(0) == LogLevel.Warning) - .MustHaveHappened(); - } - - private void MustNotResolveUser() - { - A.CallTo(() => userResolver.FindByIdAsync(A._, A._)) - .MustNotHaveHappened(); - } - - private void MustNotSendEmail() - { - A.CallTo(() => notificatíonSender.SendInviteAsync(A._, A._, A._)) - .MustNotHaveHappened(); - } - - private Envelope CreateEvent(RefTokenType assignerType, bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2) - { - var @event = new AppContributorAssigned - { - Actor = new RefToken(assignerType, assignerId), - AppId = NamedId.Of(DomainId.NewGuid(), appName), - ContributorId = assigneeId, - IsCreated = isNewUser, - IsAdded = isNewContributor - }; - - var envelope = Envelope.Create(@event); - - envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant()); - envelope.SetEventStreamNumber(streamNumber); - - return envelope; - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/ConfigAppLimitsProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/ConfigAppLimitsProviderTests.cs deleted file mode 100644 index 2a79cd7bc..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/ConfigAppLimitsProviderTests.cs +++ /dev/null @@ -1,195 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Plans -{ - public class ConfigAppLimitsProviderTests - { - private static readonly ConfigAppLimitsPlan InfinitePlan = new ConfigAppLimitsPlan - { - Id = "infinite", - Name = "Infinite", - MaxApiCalls = -1, - MaxAssetSize = -1, - MaxContributors = -1, - BlockingApiCalls = -1 - }; - - private static readonly ConfigAppLimitsPlan FreePlan = new ConfigAppLimitsPlan - { - Id = "free", - Name = "Free", - MaxApiCalls = 50000, - MaxAssetSize = 1024 * 1024 * 10, - MaxContributors = 2, - BlockingApiCalls = 50000, - IsFree = true - }; - - private static readonly ConfigAppLimitsPlan BasicPlan = new ConfigAppLimitsPlan - { - Id = "basic", - Name = "Basic", - MaxApiCalls = 150000, - MaxAssetSize = 1024 * 1024 * 2, - MaxContributors = 5, - YearlyCosts = "100€", - YearlyId = "basic_yearly", - BlockingApiCalls = 150000, - IsFree = false - }; - - private static readonly ConfigAppLimitsPlan[] Plans = { BasicPlan, FreePlan }; - - [Fact] - public void Should_return_plans() - { - var sut = new ConfigAppPlansProvider(Plans); - - Plans.OrderBy(x => x.MaxApiCalls).Should().BeEquivalentTo(sut.GetAvailablePlans()); - } - - [Theory] - [InlineData(null)] - [InlineData("my-plan")] - public void Should_return_infinite_if_nothing_configured(string planId) - { - var sut = new ConfigAppPlansProvider(Enumerable.Empty()); - - var result = sut.GetPlanForApp(CreateApp(planId)); - - result.Should().BeEquivalentTo((InfinitePlan, "infinite")); - } - - [Fact] - public void Should_return_free_plan() - { - var sut = new ConfigAppPlansProvider(Plans); - - var plan = sut.GetFreePlan(); - - plan.Should().BeEquivalentTo(FreePlan); - } - - [Fact] - public void Should_return_infinite_plan_for_free_plan_if_not_found() - { - var sut = new ConfigAppPlansProvider(Enumerable.Empty()); - - var plan = sut.GetFreePlan(); - - plan.Should().NotBeNull(); - } - - [Fact] - public void Should_return_fitting_app_plan() - { - var sut = new ConfigAppPlansProvider(Plans); - - var result = sut.GetPlanForApp(CreateApp("basic")); - - result.Should().BeEquivalentTo((BasicPlan, "basic")); - } - - [Fact] - public void Should_return_fitting_yearly_app_plan() - { - var sut = new ConfigAppPlansProvider(Plans); - - var result = sut.GetPlanForApp(CreateApp("basic_yearly")); - - result.Should().BeEquivalentTo((BasicPlan, "basic_yearly")); - } - - [Fact] - public void Should_smallest_plan_if_none_fits() - { - var sut = new ConfigAppPlansProvider(Plans); - - var result = sut.GetPlanForApp(CreateApp("enterprise")); - - result.Should().BeEquivalentTo((FreePlan, "free")); - } - - [Fact] - public void Should_return_second_plan_for_upgrade_if_plan_is_null() - { - var sut = new ConfigAppPlansProvider(Plans); - - var upgradePlan = sut.GetPlanUpgrade(null); - - upgradePlan.Should().BeEquivalentTo(BasicPlan); - } - - [Fact] - public void Should_return_second_plan_for_upgrade_if_plan_not_found() - { - var sut = new ConfigAppPlansProvider(Plans); - - var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("enterprise")); - - upgradePlan.Should().BeEquivalentTo(BasicPlan); - } - - [Fact] - public void Should_not_return_plan_for_upgrade_if_plan_is_highest_plan() - { - var sut = new ConfigAppPlansProvider(Plans); - - var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("basic")); - - Assert.Null(upgradePlan); - } - - [Fact] - public void Should_return_next_plan_if_plan_is_upgradeable() - { - var sut = new ConfigAppPlansProvider(Plans); - - var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("free")); - - upgradePlan.Should().BeEquivalentTo(BasicPlan); - } - - [Fact] - public void Should_check_plan_exists() - { - var sut = new ConfigAppPlansProvider(Plans); - - Assert.True(sut.IsConfiguredPlan("basic")); - Assert.True(sut.IsConfiguredPlan("free")); - - Assert.False(sut.IsConfiguredPlan("infinite")); - Assert.False(sut.IsConfiguredPlan("invalid")); - Assert.False(sut.IsConfiguredPlan(null)); - } - - private static IAppEntity CreateApp(string plan) - { - var app = A.Dummy(); - - if (plan != null) - { - A.CallTo(() => app.Plan) - .Returns(new AppPlan(RefToken.User("me"), plan)); - } - else - { - A.CallTo(() => app.Plan) - .Returns(null); - } - - return app; - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageGateTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageGateTests.cs deleted file mode 100644 index 4da7957fd..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageGateTests.cs +++ /dev/null @@ -1,167 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.UsageTracking; -using Squidex.Messaging; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Plans -{ - public class UsageGateTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IAppEntity appEntity; - private readonly IAppLimitsPlan appPlan = A.Fake(); - private readonly IAppPlansProvider appPlansProvider = A.Fake(); - private readonly IApiUsageTracker usageTracker = A.Fake(); - private readonly IMessageBus messaging = A.Fake(); - private readonly string clientId = Guid.NewGuid().ToString(); - private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly UsageGate sut; - private DateTime today = new DateTime(2020, 10, 3); - private long apiCallsBlocking; - private long apiCallsMax; - private long apiCallsCurrent; - - public UsageGateTests() - { - appEntity = Mocks.App(appId); - - ct = cts.Token; - - A.CallTo(() => appPlansProvider.GetPlan(null)) - .Returns(appPlan); - - A.CallTo(() => appPlansProvider.GetPlanForApp(appEntity)) - .Returns((appPlan, "free")); - - A.CallTo(() => appPlan.MaxApiCalls) - .ReturnsLazily(x => apiCallsMax); - - A.CallTo(() => appPlan.BlockingApiCalls) - .ReturnsLazily(x => apiCallsBlocking); - - A.CallTo(() => usageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A._, ct)) - .ReturnsLazily(x => Task.FromResult(apiCallsCurrent)); - - sut = new UsageGate(appPlansProvider, usageTracker, messaging); - } - - [Fact] - public async Task Should_return_true_if_over_client_limit() - { - A.CallTo(() => appEntity.Clients) - .Returns(AppClients.Empty.Add(clientId, clientId).Update(clientId, apiCallsLimit: 1000)); - - apiCallsCurrent = 1000; - apiCallsBlocking = 1600; - apiCallsMax = 1600; - - var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today, ct); - - Assert.True(isBlocked); - - A.CallTo(() => messaging.PublishAsync(A._, null, ct)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_true_if_over_blocking_limit() - { - apiCallsCurrent = 1000; - apiCallsBlocking = 600; - apiCallsMax = 600; - - var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today, ct); - - Assert.True(isBlocked); - - A.CallTo(() => messaging.PublishAsync(A._, null, ct)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_false_if_below_blocking_limit() - { - apiCallsCurrent = 100; - apiCallsBlocking = 1600; - apiCallsMax = 1600; - - var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today, ct); - - Assert.False(isBlocked); - - A.CallTo(() => messaging.PublishAsync(A._, null, A._)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_return_false_and_notify_if_about_to_over_included_contingent() - { - apiCallsCurrent = 1200; // in 10 days = 4000 / month - apiCallsBlocking = 5000; - apiCallsMax = 3000; - - var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today, ct); - - Assert.False(isBlocked); - - A.CallTo(() => messaging.PublishAsync(A._, null, ct)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_false_and_notify_if_about_to_over_included_contingent_but_no_max_given() - { - apiCallsCurrent = 1200; // in 10 days = 4000 / month - apiCallsBlocking = 5000; - apiCallsMax = 0; - - var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today, ct); - - Assert.False(isBlocked); - - A.CallTo(() => messaging.PublishAsync(A._, null, ct)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_only_notify_once_if_about_to_be_over_included_contingent() - { - apiCallsCurrent = 1200; // in 10 days = 4000 / month - apiCallsBlocking = 5000; - apiCallsMax = 3000; - - await sut.IsBlockedAsync(appEntity, clientId, today, ct); - await sut.IsBlockedAsync(appEntity, clientId, today, ct); - - A.CallTo(() => messaging.PublishAsync(A._, null, ct)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_not_notify_if_lower_than_10_percent() - { - today = new DateTime(2020, 10, 2); - - apiCallsCurrent = 220; // in 3 days = 3300 / month - apiCallsBlocking = 5000; - apiCallsMax = 3000; - - await sut.IsBlockedAsync(appEntity, clientId, today, ct); - await sut.IsBlockedAsync(appEntity, clientId, today, ct); - - A.CallTo(() => messaging.PublishAsync(A._, null, A._)) - .MustNotHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs index 9ff727c40..a0d427f3a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs @@ -39,13 +39,13 @@ namespace Squidex.Domain.Apps.Entities.Apps Mocks.Schema(appId, NamedId.Of(DomainId.NewGuid(), "schema2")) }); - var result = await sut.GetPermissionsAsync(app); + var actual = await sut.GetPermissionsAsync(app); - Assert.True(result.Contains("*")); - Assert.True(result.Contains("clients.read")); - Assert.True(result.Contains("schemas.*.update")); - Assert.True(result.Contains("schemas.schema1.update")); - Assert.True(result.Contains("schemas.schema2.update")); + Assert.True(actual.Contains("*")); + Assert.True(actual.Contains("clients.read")); + Assert.True(actual.Contains("schemas.*.update")); + Assert.True(actual.Contains("schemas.schema1.update")); + Assert.True(actual.Contains("schemas.schema2.update")); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs index 2876a54a8..92fbaab3e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -88,9 +88,9 @@ namespace Squidex.Domain.Apps.Entities.Assets new AssetEntity() }.ToAsyncEnumerable()); - var result = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); + var actual = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); - var typed = result.OfType().ToList(); + var typed = actual.OfType().ToList(); Assert.Equal(2, typed.Count); Assert.Equal(2, typed.Count(x => x.Type == EnrichedAssetEventType.Created && x.Name == "AssetQueried")); @@ -107,9 +107,9 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => assetLoader.GetAsync(ctx.AppId.Id, @event.AssetId, 12, ct)) .Returns(new AssetEntity()); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); - var enrichedEvent = (EnrichedAssetEvent)result.Single(); + var enrichedEvent = (EnrichedAssetEvent)actual.Single(); Assert.Equal(type, enrichedEvent.Type); Assert.Equal(@event.Actor, enrichedEvent.Actor); @@ -124,9 +124,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { var @event = new EnrichedAssetEvent(); - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -137,9 +137,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { var @event = new EnrichedAssetEvent(); - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -150,9 +150,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { var @event = new EnrichedAssetEvent(); - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.False(result); + Assert.False(actual); }); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs index acc37931f..222c183b7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs @@ -9,11 +9,11 @@ using FakeItEasy; using FluentAssertions; using NodaTime; using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; -using Squidex.Infrastructure.UsageTracking; using Xunit; namespace Squidex.Domain.Apps.Entities.Assets @@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly IAssetLoader assetLoader = A.Fake(); private readonly ISnapshotStore store = A.Fake>(); private readonly ITagService tagService = A.Fake(); - private readonly IUsageTracker usageTracker = A.Fake(); + private readonly IAppUsageGate appUsageGate = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly DomainId assetId = DomainId.NewGuid(); private readonly DomainId assetKey; @@ -33,10 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { assetKey = DomainId.Combine(appId, assetId); - A.CallTo(() => usageTracker.FallbackCategory) - .Returns("*"); - - sut = new AssetUsageTracker(usageTracker, assetLoader, tagService, store); + sut = new AssetUsageTracker(appUsageGate, assetLoader, tagService, store); } [Fact] @@ -63,58 +60,6 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(nameof(AssetUsageTracker), consumer.Name); } - [Fact] - public async Task Should_get_total_size_from_summary_date() - { - A.CallTo(() => usageTracker.GetAsync($"{appId.Id}_Assets", default, default, null, default)) - .Returns(new Counters { ["TotalSize"] = 2048 }); - - var size = await sut.GetTotalSizeAsync(appId.Id); - - Assert.Equal(2048, size); - } - - [Theory] - [InlineData("*")] - [InlineData("Default")] - public async Task Should_get_counters_from_categories(string category) - { - var dateFrom = new DateTime(2018, 01, 05); - var dateTo = dateFrom.AddDays(3); - - A.CallTo(() => usageTracker.QueryAsync($"{appId.Id}_Assets", dateFrom, dateTo, default)) - .Returns(new Dictionary> - { - [category] = new List<(DateTime, Counters)> - { - (dateFrom.AddDays(0), new Counters - { - ["TotalSize"] = 128, - ["TotalAssets"] = 2 - }), - (dateFrom.AddDays(1), new Counters - { - ["TotalSize"] = 256, - ["TotalAssets"] = 3 - }), - (dateFrom.AddDays(2), new Counters - { - ["TotalSize"] = 512, - ["TotalAssets"] = 4 - }) - } - }); - - var result = await sut.QueryAsync(appId.Id, dateFrom, dateTo); - - result.Should().BeEquivalentTo(new List - { - new AssetStats(dateFrom.AddDays(0), 2, 128), - new AssetStats(dateFrom.AddDays(1), 3, 256), - new AssetStats(dateFrom.AddDays(2), 4, 512) - }); - } - public static IEnumerable EventData() { yield return new object[] @@ -135,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Assets [Theory] [MemberData(nameof(EventData))] - public async Task Should_increase_usage_if_asset_created(AssetEvent @event, long sizeDiff, long countDiff) + public async Task Should_increase_usage_if_for_event(AssetEvent @event, long sizeDiff, long countDiff) { var date = DateTime.UtcNow.Date.AddDays(13); @@ -145,25 +90,10 @@ namespace Squidex.Domain.Apps.Entities.Assets Envelope.Create(@event) .SetTimestamp(Instant.FromDateTimeUtc(date)); - Counters? countersSummary = null; - Counters? countersDate = null; - - A.CallTo(() => usageTracker.TrackAsync(default, $"{appId.Id}_Assets", null, A._, default)) - .Invokes(x => countersSummary = x.GetArgument(3)); - - A.CallTo(() => usageTracker.TrackAsync(date, $"{appId.Id}_Assets", null, A._, default)) - .Invokes(x => countersDate = x.GetArgument(3)); - await sut.On(new[] { envelope }); - var expected = new Counters - { - ["TotalSize"] = sizeDiff, - ["TotalAssets"] = countDiff - }; - - countersSummary.Should().BeEquivalentTo(expected); - countersDate.Should().BeEquivalentTo(expected); + A.CallTo(() => appUsageGate.TrackAssetAsync(appId.Id, date, sizeDiff, countDiff, default)) + .MustHaveHappened(); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs index b71d953d9..a4a6fd5fd 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs @@ -92,9 +92,9 @@ namespace Squidex.Domain.Apps.Entities.Assets Text: {assets[1].FileName} {assets[1].Id} "; - var result = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -114,9 +114,9 @@ namespace Squidex.Domain.Apps.Entities.Assets Text: {assets[1].FileName} {assets[1].Id} "; - var result = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Theory] @@ -136,9 +136,9 @@ namespace Squidex.Domain.Apps.Entities.Assets Text: hello+assets "; - var result = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -155,9 +155,9 @@ namespace Squidex.Domain.Apps.Entities.Assets Text: ErrorTooBig "; - var result = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, null, A._, A._, A._)) .MustNotHaveHappened(); @@ -190,9 +190,9 @@ namespace Squidex.Domain.Apps.Entities.Assets Text: hello+assets "; - var result = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -211,9 +211,9 @@ namespace Squidex.Domain.Apps.Entities.Assets Text: Hash "; - var result = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -230,9 +230,9 @@ namespace Squidex.Domain.Apps.Entities.Assets Text: ErrorTooBig "; - var result = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, null, A._, A._, A._)) .MustNotHaveHappened(); @@ -252,9 +252,9 @@ namespace Squidex.Domain.Apps.Entities.Assets Text: NoImage "; - var result = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, null, A._, A._, A._)) .MustNotHaveHappened(); @@ -287,9 +287,9 @@ namespace Squidex.Domain.Apps.Entities.Assets Text: Hash "; - var result = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } private void SetupBlurHash(AssetRef asset, string hash) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs index af425b6cd..346721556 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs @@ -94,14 +94,14 @@ namespace Squidex.Domain.Apps.Entities.Assets var script = @" getAsset(data.assets.iv[0], function (assets) { - var result1 = `Text: ${assets[0].fileName} ${assets[0].id}`; + var actual1 = `Text: ${assets[0].fileName} ${assets[0].id}`; - complete(`${result1}`); + complete(`${actual1}`); });"; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -116,15 +116,15 @@ namespace Squidex.Domain.Apps.Entities.Assets var script = @" getAssets(data.assets.iv, function (assets) { - var result1 = `Text: ${assets[0].fileName} ${assets[0].id}`; - var result2 = `Text: ${assets[1].fileName} ${assets[1].id}`; + var actual1 = `Text: ${assets[0].fileName} ${assets[0].id}`; + var actual2 = `Text: ${assets[1].fileName} ${assets[1].id}`; - complete(`${result1}\n${result2}`); + complete(`${actual1}\n${actual2}`); });"; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Theory] @@ -142,15 +142,15 @@ namespace Squidex.Domain.Apps.Entities.Assets var script = $@" getAssets(data.assets.iv, function (assets) {{ getAssetText(assets[0], function (text) {{ - var result = `Text: ${{text}}`; + var actual = `Text: ${{text}}`; - complete(result); + complete(actual); }}, '{encoding}'); }});"; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -165,15 +165,15 @@ namespace Squidex.Domain.Apps.Entities.Assets var script = @" getAssets(data.assets.iv, function (assets) { getAssetText(assets[0], function (text) { - var result = `Text: ${text}`; + var actual = `Text: ${text}`; - complete(result); + complete(actual); }); });"; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, null, A._, A._, A._)) .MustNotHaveHappened(); @@ -204,14 +204,14 @@ namespace Squidex.Domain.Apps.Entities.Assets var script = $@" getAssetText(event, function (text) {{ - var result = `Text: ${{text}}`; + var actual = `Text: ${{text}}`; - complete(result); + complete(actual); }}, '{encoding}');"; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -228,15 +228,15 @@ namespace Squidex.Domain.Apps.Entities.Assets var script = @" getAssets(data.assets.iv, function (assets) { getAssetBlurHash(assets[0], function (text) { - var result = `Hash: ${text}`; + var actual = `Hash: ${text}`; - complete(result); + complete(actual); }); });"; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -253,15 +253,15 @@ namespace Squidex.Domain.Apps.Entities.Assets var script = @" getAssets(data.assets.iv, function (assets) { getAssetBlurHash(assets[0], function (text) { - var result = `Hash: ${text}`; + var actual = `Hash: ${text}`; - complete(result); + complete(actual); }); });"; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -278,15 +278,15 @@ namespace Squidex.Domain.Apps.Entities.Assets var script = @" getAssets(data.assets.iv, function (assets) { getAssetBlurHash(assets[0], function (text) { - var result = `Hash: ${text}`; + var actual = `Hash: ${text}`; - complete(result); + complete(actual); }); });"; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -314,14 +314,14 @@ namespace Squidex.Domain.Apps.Entities.Assets var script = @" getAssetBlurHash(event, function (text) { - var result = `Text: ${text}`; + var actual = `Text: ${text}`; - complete(result); + complete(actual); });"; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } private void SetupBlurHash(AssetRef asset, string hash) @@ -350,7 +350,7 @@ namespace Squidex.Domain.Apps.Entities.Assets .AddInvariant(JsonValue.Array(assetIds))); A.CallTo(() => assetQuery.QueryAsync( - A.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A.That.HasIds(assetIds), A._)) + A.That.Matches(x => x.App.Id == appId.Id && x.UserPrincipal == user), null, A.That.HasIds(assetIds), A._)) .Returns(ResultList.CreateFrom(2, assets)); var vars = new ScriptVars diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsSearchSourceTests.cs index a4f1b305e..c944725b6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsSearchSourceTests.cs @@ -31,20 +31,20 @@ namespace Squidex.Domain.Apps.Entities.Assets } [Fact] - public async Task Should_return_empty_results_if_user_has_no_permission() + public async Task Should_return_empty_actuals_if_user_has_no_permission() { var ctx = ContextWithPermission(); - var result = await sut.SearchAsync("logo", ctx, default); + var actual = await sut.SearchAsync("logo", ctx, default); - Assert.Empty(result); + Assert.Empty(actual); A.CallTo(() => assetQuery.QueryAsync(A._, A._, A._, A._)) .MustNotHaveHappened(); } [Fact] - public async Task Should_return_assets_results_if_found() + public async Task Should_return_assets_actuals_if_found() { var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, appId.Name); @@ -62,9 +62,9 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => assetQuery.QueryAsync(ctx, null, A.That.HasQuery("Filter: contains(fileName, 'logo'); Take: 5"), A._)) .Returns(ResultList.CreateFrom(2, asset1, asset2)); - var result = await sut.SearchAsync("logo", ctx, default); + var actual = await sut.SearchAsync("logo", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("logo1.png", SearchResultType.Asset, "assets-url1") .Add("logo2.png", SearchResultType.Asset, "assets-url2")); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs index a21436783..f503ab58f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs @@ -332,8 +332,8 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(new HashSet { - DomainId.Combine(appId.Id, assetId1), - DomainId.Combine(appId.Id, assetId2) + DomainId.Combine(appId, assetId1), + DomainId.Combine(appId, assetId2) }, rebuildAssets); } @@ -369,8 +369,8 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(new HashSet { - DomainId.Combine(appId.Id, assetFolderId1), - DomainId.Combine(appId.Id, assetFolderId2) + DomainId.Combine(appId, assetFolderId1), + DomainId.Combine(appId, assetFolderId2) }, rebuildAssetFolders); } @@ -388,14 +388,14 @@ namespace Squidex.Domain.Apps.Entities.Assets { @event.AppId = appId; - return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId.Id, @event.AssetId)); + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.AssetId)); } private Envelope AppEvent(AssetFolderEvent @event) { @event.AppId = appId; - return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId.Id, @event.AssetFolderId)); + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.AssetFolderId)); } private IUserMapping CreateUserMapping() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs index 863ad39f8..e785eb136 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs @@ -62,9 +62,9 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => assetStore.GeneratePublicUrl(fullName)) .Returns(url); - var result = sut.GeneratePublicUrl(appId, assetId, assetFileVersion, suffix); + var actual = sut.GeneratePublicUrl(appId, assetId, assetFileVersion, suffix); - Assert.Equal(url, result); + Assert.Equal(url, actual); } [Theory] @@ -80,9 +80,9 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => assetStore.GetSizeAsync(fullName, ct)) .Returns(size); - var result = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix, ct); + var actual = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix, ct); - Assert.Equal(size, result); + Assert.Equal(size, actual); } [Theory] @@ -99,9 +99,9 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => assetStore.GetSizeAsync(fullName, ct)) .Returns(size); - var result = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix, ct); + var actual = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix, ct); - Assert.Equal(size, result); + Assert.Equal(size, actual); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs index b63473bf5..41f44b852 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs @@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject } [Fact] - public async Task Should_not_invoke_enricher_for_other_result() + public async Task Should_not_invoke_enricher_for_other_actual() { await HandleAsync(new AnnotateAsset(), 12); @@ -75,29 +75,29 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject [Fact] public async Task Should_not_invoke_enricher_if_already_enriched() { - var result = new AssetEntity(); + var actual = new AssetEntity(); var context = await HandleAsync(new AnnotateAsset(), - result); + actual); A.CallTo(() => assetEnricher.EnrichAsync(A._, requestContext, A._)) .MustNotHaveHappened(); } [Fact] - public async Task Should_enrich_asset_result() + public async Task Should_enrich_asset_actual() { - var result = A.Fake(); + var actual = A.Fake(); var enriched = new AssetEntity(); - A.CallTo(() => assetEnricher.EnrichAsync(result, requestContext, ct)) + A.CallTo(() => assetEnricher.EnrichAsync(actual, requestContext, ct)) .Returns(enriched); var context = await HandleAsync(new AnnotateAsset(), - result); + actual); Assert.Same(enriched, context.Result()); } @@ -105,13 +105,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject [Fact] public async Task Create_should_upload_file() { - var result = CreateAsset(); + var actual = CreateAsset(); var context = await HandleAsync(new CreateAsset { File = file }, - result); + actual); - Assert.Same(result, context.Result()); + Assert.Same(actual, context.Result()); AssertAssetHasBeenUploaded(0); AssertMetadataEnriched(); @@ -128,21 +128,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject } [Fact] - public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_found_but_duplicate_allowed() + public async Task Create_should_not_return_duplicate_actual_if_file_with_same_hash_found_but_duplicate_allowed() { - var result = CreateAsset(); + var actual = CreateAsset(); SetupSameHashAsset(file.FileName, file.FileSize, out _); var context = await HandleAsync(new CreateAsset { File = file, Duplicate = true }, - result); + actual); - Assert.Same(result, context.Result()); + Assert.Same(actual, context.Result()); } [Fact] - public async Task Create_should_return_duplicate_result_if_file_with_same_hash_found() + public async Task Create_should_return_duplicate_actual_if_file_with_same_hash_found() { SetupSameHashAsset(file.FileName, file.FileSize, out var duplicate); @@ -182,21 +182,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject } [Fact] - public async Task Upsert_should_not_return_duplicate_result_if_file_with_same_hash_found_but_duplicate_allowed() + public async Task Upsert_should_not_return_duplicate_actual_if_file_with_same_hash_found_but_duplicate_allowed() { - var result = CreateAsset(); + var actual = CreateAsset(); SetupSameHashAsset(file.FileName, file.FileSize, out _); var context = await HandleAsync(new UpsertAsset { File = file }, - result); + actual); - Assert.Same(result, context.Result()); + Assert.Same(actual, context.Result()); } [Fact] - public async Task Upsert_should_return_duplicate_result_if_file_with_same_hash_found() + public async Task Upsert_should_return_duplicate_actual_if_file_with_same_hash_found() { SetupSameHashAsset(file.FileName, file.FileSize, out var duplicate); @@ -247,7 +247,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject .MustHaveHappened(); } - private Task HandleAsync(AssetCommand command, object result) + private Task HandleAsync(AssetCommand command, object actual) { command.AssetId = assetId; @@ -256,7 +256,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject var domainObject = A.Fake(); A.CallTo(() => domainObject.ExecuteAsync(A._, ct)) - .Returns(new CommandResult(command.AggregateId, 1, 0, result)); + .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); A.CallTo(() => domainObjectFactory.Create(command.AggregateId)) .Returns(domainObject); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs index b4033ad89..ffbf9c24f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs @@ -97,9 +97,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { var command = new CreateAsset { File = file, FileHash = "NewHash" }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(0, sut.Snapshot.FileVersion); Assert.Equal(command.FileHash, sut.Snapshot.FileHash); @@ -150,9 +150,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { var command = new UpsertAsset { File = file, FileHash = "NewHash" }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(0, sut.Snapshot.FileVersion); Assert.Equal(command.FileHash, sut.Snapshot.FileHash); @@ -183,9 +183,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(1, sut.Snapshot.FileVersion); Assert.Equal(command.FileHash, sut.Snapshot.FileHash); @@ -213,9 +213,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(1, sut.Snapshot.FileVersion); Assert.Equal(command.FileHash, sut.Snapshot.FileHash); @@ -243,9 +243,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.FileName, sut.Snapshot.FileName); @@ -265,9 +265,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Slug, sut.Snapshot.Slug); @@ -287,9 +287,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.IsProtected, sut.Snapshot.IsProtected); @@ -309,9 +309,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Metadata, sut.Snapshot.Metadata); @@ -331,9 +331,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -351,9 +351,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(parentId, sut.Snapshot.ParentId); @@ -374,9 +374,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); await ExecuteUpdateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); Assert.True(sut.Snapshot.IsDeleted); @@ -399,9 +399,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id, SearchScope.All, A._)) .Returns(false); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version); Assert.Empty(LastEvents); @@ -483,9 +483,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private async Task PublishAsync(AssetCommand command) { - var result = await sut.ExecuteAsync(CreateAssetCommand(command), default); + var actual = await sut.ExecuteAsync(CreateAssetCommand(command), default); - return result.Payload; + return actual.Payload; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs index 75bf6640c..27efa89b8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs @@ -72,9 +72,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { var command = new CreateAssetFolder { FolderName = "New Name" }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.FolderName, sut.Snapshot.FolderName); @@ -91,9 +91,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.FolderName, sut.Snapshot.FolderName); @@ -110,9 +110,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(parentId, sut.Snapshot.ParentId); @@ -129,9 +129,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); Assert.True(sut.Snapshot.IsDeleted); @@ -177,9 +177,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private async Task PublishAsync(AssetFolderCommand command) { - var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command), default); + var actual = await sut.ExecuteAsync(CreateAssetFolderCommand(command), default); - return result.Payload; + return actual.Payload; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs index 003875d65..f5ce11563 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs @@ -41,9 +41,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { var command = new BulkUpdateAssets(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -51,9 +51,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { var command = new BulkUpdateAssets { Jobs = Array.Empty() }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -65,10 +65,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject var command = BulkCommand(BulkUpdateAssetType.Annotate, id, fileName: "file"); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == id && x.FileName == "file"), ct)) .MustHaveHappened(); @@ -83,10 +83,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject var command = BulkCommand(BulkUpdateAssetType.Move, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -101,10 +101,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject var command = BulkCommand(BulkUpdateAssetType.Move, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == id), ct)) .MustHaveHappened(); @@ -119,10 +119,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject var command = BulkCommand(BulkUpdateAssetType.Move, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -137,10 +137,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject var command = BulkCommand(BulkUpdateAssetType.Delete, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.AssetId == id), ct)) @@ -156,10 +156,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject var command = BulkCommand(BulkUpdateAssetType.Delete, id: id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetMappingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetMappingTests.cs index b373255f9..e7f008c61 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetMappingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetMappingTests.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb [Fact] public void Should_map_asset() { - var user = new RefToken(RefTokenType.Subject, "1"); + var user = RefToken.User("1"); var time = SystemClock.Instance.GetCurrentInstant(); @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb [Fact] public void Should_map_asset_folder() { - var user = new RefToken(RefTokenType.Subject, "1"); + var user = RefToken.User("1"); var time = SystemClock.Instance.GetCurrentInstant(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs index 00270295e..869572725 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs @@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb var now = SystemClock.Instance.GetCurrentInstant(); - var user = new RefToken(RefTokenType.Subject, "1"); + var user = RefToken.User("1"); foreach (var appId in AppIds) { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs index fe3844c54..5939ec896 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs @@ -51,9 +51,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var source = new AssetEntity { AppId = appId }; - var result = await sut.EnrichAsync(source, requestContext, ct); + var actual = await sut.EnrichAsync(source, requestContext, ct); - Assert.Empty(result.TagNames); + Assert.Empty(actual.TagNames); } [Fact] @@ -61,9 +61,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var source = new AssetEntity { AppId = appId, Id = DomainId.NewGuid(), Version = 13 }; - var result = await sut.EnrichAsync(source, requestContext, ct); + var actual = await sut.EnrichAsync(source, requestContext, ct); - A.CallTo(() => requestCache.AddDependency(result.UniqueId, result.Version)) + A.CallTo(() => requestCache.AddDependency(actual.UniqueId, actual.Version)) .MustHaveHappened(); } @@ -87,9 +87,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries ["id2"] = "name2" }); - var result = await sut.EnrichAsync(source, requestContext, ct); + var actual = await sut.EnrichAsync(source, requestContext, ct); - Assert.Equal(new HashSet { "name1", "name2" }, result.TagNames); + Assert.Equal(new HashSet { "name1", "name2" }, actual.TagNames); } [Fact] @@ -105,9 +105,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AppId = appId }; - var result = await sut.EnrichAsync(source, requestContext.Clone(b => b.WithoutAssetEnrichment()), ct); + var actual = await sut.EnrichAsync(source, requestContext.Clone(b => b.WithoutAssetEnrichment()), ct); - Assert.Null(result.TagNames); + Assert.Null(actual.TagNames); } [Fact] @@ -130,9 +130,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetMetadataSource2.Format(A._)) .Returns(new[] { "metadata2", "metadata3" }); - var result = await sut.EnrichAsync(source, requestContext, ct); + var actual = await sut.EnrichAsync(source, requestContext, ct); - Assert.Equal("metadata1, metadata2, metadata3, 2 kB", result.MetadataText); + Assert.Equal("metadata1, metadata2, metadata3, 2 kB", actual.MetadataText); } [Fact] @@ -166,10 +166,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries ["id3"] = "name3" }); - var result = await sut.EnrichAsync(new[] { source1, source2 }, requestContext, ct); + var actual = await sut.EnrichAsync(new[] { source1, source2 }, requestContext, ct); - Assert.Equal(new HashSet { "name1", "name2" }, result[0].TagNames); - Assert.Equal(new HashSet { "name2", "name3" }, result[1].TagNames); + Assert.Equal(new HashSet { "name1", "name2" }, actual[0].TagNames); + Assert.Equal(new HashSet { "name2", "name3" }, actual[1].TagNames); } [Fact] @@ -180,9 +180,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AppId = appId }; - var result = await sut.EnrichAsync(new[] { source }, new Context(Mocks.FrontendUser(), Mocks.App(appId)), ct); + var actual = await sut.EnrichAsync(new[] { source }, new Context(Mocks.FrontendUser(), Mocks.App(appId)), ct); - Assert.NotNull(result[0].EditToken); + Assert.NotNull(actual[0].EditToken); A.CallTo(() => urlGenerator.Root()) .MustHaveHappened(); @@ -196,9 +196,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AppId = appId }; - var result = await sut.EnrichAsync(new[] { source }, requestContext, ct); + var actual = await sut.EnrichAsync(new[] { source }, requestContext, ct); - Assert.NotNull(result[0].EditToken); + Assert.NotNull(actual[0].EditToken); A.CallTo(() => urlGenerator.Root()) .MustHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs index 173de1609..d5f95c037 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs @@ -81,9 +81,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => domainObject.GetSnapshotAsync(EtagVersion.Any, ct)) .Returns(asset); - var result = await sut.GetAsync(appId, id, EtagVersion.Any, ct); + var actual = await sut.GetAsync(appId, id, EtagVersion.Any, ct); - Assert.Same(asset, result); + Assert.Same(asset, actual); } [Fact] @@ -94,9 +94,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) .Returns(asset); - var result = await sut.GetAsync(appId, id, 10, ct); + var actual = await sut.GetAsync(appId, id, 10, ct); - Assert.Same(asset, result); + Assert.Same(asset, actual); } [Fact] @@ -107,9 +107,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => domainObjectCache.GetAsync(DomainId.Combine(appId, id), 10, ct)) .Returns(content); - var result = await sut.GetAsync(appId, id, 10, ct); + var actual = await sut.GetAsync(appId, id, 10, ct); - Assert.Same(content, result); + Assert.Same(content, actual); A.CallTo(() => domainObjectFactory.Create(uniqueId)) .MustNotHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs index 88cddbb5a..da4902958 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs @@ -58,9 +58,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug", A._)) .Returns(asset); - var result = await sut.FindBySlugAsync(requestContext, "slug", ct); + var actual = await sut.FindBySlugAsync(requestContext, "slug", ct); - AssertAsset(asset, result); + AssertAsset(asset, actual); } [Fact] @@ -71,9 +71,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug", A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindBySlugAsync(requestContext, "slug", ct); + var actual = await sut.FindBySlugAsync(requestContext, "slug", ct); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -84,9 +84,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id, A._)) .Returns(asset); - var result = await sut.FindAsync(requestContext, asset.Id, ct: ct); + var actual = await sut.FindAsync(requestContext, asset.Id, ct: ct); - AssertAsset(asset, result); + AssertAsset(asset, actual); } [Fact] @@ -97,9 +97,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id, A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindAsync(requestContext, asset.Id, ct: ct); + var actual = await sut.FindAsync(requestContext, asset.Id, ct: ct); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -110,9 +110,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2, A._)) .Returns(asset); - var result = await sut.FindAsync(requestContext, asset.Id, 2, ct); + var actual = await sut.FindAsync(requestContext, asset.Id, 2, ct); - AssertAsset(asset, result); + AssertAsset(asset, actual); } [Fact] @@ -123,9 +123,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2, A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindAsync(requestContext, asset.Id, 2, ct); + var actual = await sut.FindAsync(requestContext, asset.Id, 2, ct); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -136,9 +136,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetAsync(asset.Id, A._)) .Returns(asset); - var result = await sut.FindGlobalAsync(requestContext, asset.Id, ct); + var actual = await sut.FindGlobalAsync(requestContext, asset.Id, ct); - AssertAsset(asset, result); + AssertAsset(asset, actual); } [Fact] @@ -149,9 +149,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetAsync(asset.Id, A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindGlobalAsync(requestContext, asset.Id, ct); + var actual = await sut.FindGlobalAsync(requestContext, asset.Id, ct); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -162,9 +162,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123, A._)) .Returns(asset); - var result = await sut.FindByHashAsync(requestContext, "hash", "name", 123, ct); + var actual = await sut.FindByHashAsync(requestContext, "hash", "name", 123, ct); - AssertAsset(asset, result); + AssertAsset(asset, actual); } [Fact] @@ -175,9 +175,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123, A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindByHashAsync(requestContext, "hash", "name", 123, ct); + var actual = await sut.FindByHashAsync(requestContext, "hash", "name", 123, ct); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -193,12 +193,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, q, A._)) .Returns(ResultList.CreateFrom(8, asset1, asset2)); - var result = await sut.QueryAsync(requestContext, parentId, q, ct); + var actual = await sut.QueryAsync(requestContext, parentId, q, ct); - Assert.Equal(8, result.Total); + Assert.Equal(8, actual.Total); - AssertAsset(asset1, result[0]); - AssertAsset(asset2, result[1]); + AssertAsset(asset1, actual[0]); + AssertAsset(asset2, actual[1]); } [Fact] @@ -211,9 +211,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.QueryAsync(appId.Id, parentId, A._)) .Returns(assetFolders); - var result = await sut.QueryAssetFoldersAsync(requestContext, parentId, ct); + var actual = await sut.QueryAssetFoldersAsync(requestContext, parentId, ct); - Assert.Same(assetFolders, result); + Assert.Same(assetFolders, actual); } [Fact] @@ -226,9 +226,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.QueryAsync(appId.Id, parentId, A._)) .Returns(assetFolders); - var result = await sut.QueryAssetFoldersAsync(appId.Id, parentId, ct); + var actual = await sut.QueryAssetFoldersAsync(appId.Id, parentId, ct); - Assert.Same(assetFolders, result); + Assert.Same(assetFolders, actual); } [Fact] @@ -240,9 +240,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A._)) .Returns(folder1); - var result = await sut.FindAssetFolderAsync(appId.Id, folderId1, ct); + var actual = await sut.FindAssetFolderAsync(appId.Id, folderId1, ct); - Assert.Equal(result, new[] { folder1 }); + Assert.Equal(actual, new[] { folder1 }); } [Fact] @@ -265,9 +265,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId3, A._)) .Returns(folder3); - var result = await sut.FindAssetFolderAsync(appId.Id, folderId3, ct); + var actual = await sut.FindAssetFolderAsync(appId.Id, folderId3, ct); - Assert.Equal(result, new[] { folder1, folder2, folder3 }); + Assert.Equal(actual, new[] { folder1, folder2, folder3 }); } [Fact] @@ -278,9 +278,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindAssetFolderAsync(appId.Id, folderId1, ct); + var actual = await sut.FindAssetFolderAsync(appId.Id, folderId1, ct); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -298,9 +298,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId2, A._)) .Returns(folder2); - var result = await sut.FindAssetFolderAsync(appId.Id, folderId2, ct); + var actual = await sut.FindAssetFolderAsync(appId.Id, folderId2, ct); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -318,16 +318,16 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId2, A._)) .Returns(folder2); - var result = await sut.FindAssetFolderAsync(appId.Id, folderId2, ct); + var actual = await sut.FindAssetFolderAsync(appId.Id, folderId2, ct); - Assert.Empty(result); + Assert.Empty(actual); } - private static void AssertAsset(IAssetEntity source, IEnrichedAssetEntity? result) + private static void AssertAsset(IAssetEntity source, IEnrichedAssetEntity? actual) { - Assert.NotNull(result); - Assert.NotSame(source, result); - Assert.Equal(source.AssetId, result?.AssetId); + Assert.NotNull(actual); + Assert.NotSame(source, actual); + Assert.Equal(source.AssetId, actual?.AssetId); } private static IAssetFolderEntity CreateFolder(DomainId id, DomainId parentId = default) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs index 8e2debef0..402583b4b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs @@ -125,9 +125,9 @@ namespace Squidex.Domain.Apps.Entities.Backup } }; - var result = await sut.GetRestoreAsync(); + var actual = await sut.GetRestoreAsync(); - result.Should().BeEquivalentTo(stateRestore.Snapshot.Job); + actual.Should().BeEquivalentTo(stateRestore.Snapshot.Job); } [Fact] @@ -142,9 +142,9 @@ namespace Squidex.Domain.Apps.Entities.Backup stateBackup.Snapshot.Jobs.Add(job); - var result = await sut.GetBackupsAsync(appId); + var actual = await sut.GetBackupsAsync(appId); - result.Should().BeEquivalentTo(stateBackup.Snapshot.Jobs); + actual.Should().BeEquivalentTo(stateBackup.Snapshot.Jobs); } [Fact] @@ -159,9 +159,9 @@ namespace Squidex.Domain.Apps.Entities.Backup stateBackup.Snapshot.Jobs.Add(job); - var result = await sut.GetBackupAsync(appId, backupId); + var actual = await sut.GetBackupAsync(appId, backupId); - result.Should().BeEquivalentTo(job); + actual.Should().BeEquivalentTo(job); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/StreamMapperTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/StreamMapperTests.cs index cccc84aa2..f37f1d8bc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/StreamMapperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/StreamMapperTests.cs @@ -28,33 +28,33 @@ namespace Squidex.Domain.Apps.Entities.Backup [Fact] public void Should_map_old_app_id() { - var result = sut.Map($"app-{appIdOld}"); + var actual = sut.Map($"app-{appIdOld}"); - Assert.Equal(($"app-{appId}", appId), result); + Assert.Equal(($"app-{appId}", appId), actual); } [Fact] public void Should_map_old_app_broken_id() { - var result = sut.Map($"app-{appIdOld}--{appIdOld}"); + var actual = sut.Map($"app-{appIdOld}--{appIdOld}"); - Assert.Equal(($"app-{appId}", appId), result); + Assert.Equal(($"app-{appId}", appId), actual); } [Fact] public void Should_map_non_app_id() { - var result = sut.Map($"content-{appIdOld}--123"); + var actual = sut.Map($"content-{appIdOld}--123"); - Assert.Equal(($"content-{appId}--123", DomainId.Create($"{appId}--123")), result); + Assert.Equal(($"content-{appId}--123", DomainId.Create($"{appId}--123")), actual); } [Fact] public void Should_map_non_app_id_with_double_slash() { - var result = sut.Map($"content-{appIdOld}--other--id"); + var actual = sut.Map($"content-{appIdOld}--other--id"); - Assert.Equal(($"content-{appId}--other--id", DomainId.Create($"{appId}--other--id")), result); + Assert.Equal(($"content-{appId}--other--id", DomainId.Create($"{appId}--other--id")), actual); } [Fact] @@ -62,9 +62,9 @@ namespace Squidex.Domain.Apps.Entities.Backup { var id = DomainId.NewGuid(); - var result = sut.Map($"content-{id}"); + var actual = sut.Map($"content-{id}"); - Assert.Equal(($"content-{appId}--{id}", DomainId.Create($"{appId}--{id}")), result); + Assert.Equal(($"content-{appId}--{id}", DomainId.Create($"{appId}--{id}")), actual); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs new file mode 100644 index 000000000..4c8b58969 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs @@ -0,0 +1,134 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FluentAssertions; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Billing +{ + public class ConfigPlansProviderTests + { + private static readonly Plan InfinitePlan = new Plan + { + Id = "infinite", + Name = "Infinite", + MaxApiCalls = -1, + MaxAssetSize = -1, + MaxContributors = -1, + BlockingApiCalls = -1 + }; + + private static readonly Plan FreePlan = new Plan + { + Id = "free", + Name = "Free", + MaxApiCalls = 50000, + MaxAssetSize = 1024 * 1024 * 10, + MaxContributors = 2, + BlockingApiCalls = 50000, + IsFree = true + }; + + private static readonly Plan BasicPlan = new Plan + { + Id = "basic", + Name = "Basic", + MaxApiCalls = 150000, + MaxAssetSize = 1024 * 1024 * 2, + MaxContributors = 5, + YearlyCosts = "100€", + YearlyId = "basic_yearly", + BlockingApiCalls = 150000, + IsFree = false + }; + + private static readonly Plan[] Plans = { BasicPlan, FreePlan }; + + [Fact] + public void Should_return_plans() + { + var sut = new ConfigPlansProvider(Plans); + + sut.GetAvailablePlans().Should().BeEquivalentTo(Plans.OrderBy(x => x.MaxApiCalls)); + } + + [Theory] + [InlineData(null)] + [InlineData("my-plan")] + public void Should_return_infinite_if_nothing_configured(string planId) + { + var sut = new ConfigPlansProvider(Enumerable.Empty()); + + var actual = sut.GetActualPlan(planId); + + actual.Should().BeEquivalentTo((InfinitePlan, "infinite")); + } + + [Fact] + public void Should_return_free_plan() + { + var sut = new ConfigPlansProvider(Plans); + + var plan = sut.GetFreePlan(); + + plan.Should().BeEquivalentTo(FreePlan); + } + + [Fact] + public void Should_return_infinite_plan_for_free_plan_if_not_found() + { + var sut = new ConfigPlansProvider(Enumerable.Empty()); + + var plan = sut.GetFreePlan(); + + plan.Should().NotBeNull(); + } + + [Fact] + public void Should_return_fitting_app_plan() + { + var sut = new ConfigPlansProvider(Plans); + + var actual = sut.GetActualPlan("basic"); + + actual.Should().BeEquivalentTo((BasicPlan, "basic")); + } + + [Fact] + public void Should_return_fitting_yearly_app_plan() + { + var sut = new ConfigPlansProvider(Plans); + + var actual = sut.GetActualPlan("basic_yearly"); + + actual.Should().BeEquivalentTo((BasicPlan, "basic_yearly")); + } + + [Fact] + public void Should_smallest_plan_if_none_fits() + { + var sut = new ConfigPlansProvider(Plans); + + var actual = sut.GetActualPlan("enterprise"); + + actual.Should().BeEquivalentTo((FreePlan, "free")); + } + + [Fact] + public void Should_check_plan_exists() + { + var sut = new ConfigPlansProvider(Plans); + + Assert.True(sut.IsConfiguredPlan("basic")); + Assert.True(sut.IsConfiguredPlan("free")); + + Assert.False(sut.IsConfiguredPlan("infinite")); + Assert.False(sut.IsConfiguredPlan("invalid")); + Assert.False(sut.IsConfiguredPlan(null)); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/NoopAppPlanBillingManagerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/NoopBillingManagerTests.cs similarity index 51% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/NoopAppPlanBillingManagerTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/NoopBillingManagerTests.cs index 6601b4218..c76b6d0fc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/NoopAppPlanBillingManagerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/NoopBillingManagerTests.cs @@ -5,13 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps.Plans +namespace Squidex.Domain.Apps.Entities.Billing { - public class NoopAppPlanBillingManagerTests + public class NoopBillingManagerTests { - private readonly NoopAppPlanBillingManager sut = new NoopAppPlanBillingManager(); + private readonly NoopBillingManager sut = new NoopBillingManager(); [Fact] public void Should_not_have_portal() @@ -25,26 +26,46 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans await sut.SubscribeAsync(null!, null!, null!); } + [Fact] + public async Task Should_do_nothing_if_subscribing_to_team() + { + await sut.SubscribeAsync(null!, default(DomainId), null!); + } + [Fact] public async Task Should_do_nothing_if_unsubscribing() { await sut.UnsubscribeAsync(null!, null!); } + [Fact] + public async Task Should_do_nothing_if_unsubscribing_from_team() + { + await sut.UnsubscribeAsync(null!, default(DomainId)); + } + [Fact] public async Task Should_not_return_portal_link() { - var result = await sut.GetPortalLinkAsync(null!); + var actual = await sut.GetPortalLinkAsync(null!); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] public async Task Should_do_nothing_if_checking_for_redirect() { - var result = await sut.MustRedirectToPortalAsync(null!, null!, null); + var actual = await sut.MustRedirectToPortalAsync(null!, null!, null); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_do_nothing_if_checking_for_redirect_for_team() + { + var actual = await sut.MustRedirectToPortalAsync(null!, default(DomainId), null); - Assert.Null(result); + Assert.Null(actual); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs new file mode 100644 index 000000000..280c00371 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs @@ -0,0 +1,441 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using FluentAssertions; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.UsageTracking; +using Squidex.Messaging; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Billing +{ + public class UsageGateTests + { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IMessageBus messaging = A.Fake(); + private readonly IApiUsageTracker apiUsageTracker = A.Fake(); + private readonly IAppEntity appWithoutTeam; + private readonly IAppEntity appWithTeam; + private readonly IAppProvider appProvider = A.Fake(); + private readonly IBillingPlans billingPlans = A.Fake(); + private readonly IUsageTracker usageTracker = A.Fake(); + private readonly string clientId = Guid.NewGuid().ToString(); + private readonly DomainId teamId = DomainId.NewGuid(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly DateTime today = new DateTime(2020, 10, 3); + private readonly Plan planFree = new Plan { Id = "free" }; + private readonly Plan planPaid = new Plan { Id = "paid" }; + private readonly UsageGate sut; + + public UsageGateTests() + { + appWithoutTeam = Mocks.App(appId); + appWithTeam = Mocks.App(appId); + + ct = cts.Token; + + A.CallTo(() => appWithTeam.TeamId) + .Returns(teamId); + + A.CallTo(() => billingPlans.GetActualPlan(A._)) + .ReturnsLazily(x => (planFree, planFree.Id)); + + A.CallTo(() => billingPlans.GetActualPlan(planPaid.Id)) + .ReturnsLazily(x => (planPaid, planPaid.Id)); + + A.CallTo(() => usageTracker.FallbackCategory) + .Returns("*"); + + sut = new UsageGate(appProvider, apiUsageTracker, billingPlans, messaging, usageTracker); + } + + [Fact] + public async Task Should_delete_app_asset_usage() + { + await sut.DeleteAssetUsageAsync(appId.Id, ct); + + A.CallTo(() => usageTracker.DeleteAsync($"{appId.Id}_Assets", ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_delete_assets_usage() + { + await sut.DeleteAssetsUsageAsync(ct); + + A.CallTo(() => usageTracker.DeleteByKeyPatternAsync("^([a-zA-Z0-9]+)_Assets", ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_get_free_plan_for_app() + { + var plan = await sut.GetPlanForAppAsync(appWithoutTeam, ct); + + Assert.Equal((planFree, planFree.Id, null), plan); + } + + [Fact] + public async Task Should_get_free_plan_for_app_with_team() + { + var team = A.Fake(); + + A.CallTo(() => appProvider.GetTeamAsync(teamId, ct)) + .Returns(team); + + A.CallTo(() => team.Id) + .Returns(teamId); + + var plan = await sut.GetPlanForAppAsync(appWithTeam, ct); + + Assert.Equal((planFree, planFree.Id, teamId), plan); + } + + [Fact] + public async Task Should_get_paid_plan_for_app() + { + A.CallTo(() => appWithoutTeam.Plan) + .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); + + var plan = await sut.GetPlanForAppAsync(appWithoutTeam, ct); + + Assert.Equal((planPaid, planPaid.Id, null), plan); + } + + [Fact] + public async Task Should_get_paid_plan_for_app_id() + { + A.CallTo(() => appProvider.GetAppAsync(appWithoutTeam.Id, true, ct)) + .Returns(appWithoutTeam); + + A.CallTo(() => appWithoutTeam.Plan) + .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); + + var plan = await sut.GetPlanForAppAsync(appWithoutTeam.Id, ct); + + Assert.Equal((planPaid, planPaid.Id, null), plan); + } + + [Fact] + public async Task Should_get_paid_plan_for_app_with_team() + { + var team = A.Fake(); + + A.CallTo(() => appProvider.GetTeamAsync(teamId, ct)) + .Returns(team); + + A.CallTo(() => team.Id) + .Returns(teamId); + + A.CallTo(() => team.Plan) + .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); + + var plan = await sut.GetPlanForAppAsync(appWithTeam, ct); + + Assert.Equal((planPaid, planPaid.Id, teamId), plan); + } + + [Fact] + public async Task Should_block_with_true_if_over_client_limit() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 1600, MaxApiCalls = 1600 }; + + A.CallTo(() => billingPlans.GetActualPlan(A._)) + .ReturnsLazily(x => (plan, plan.Id)); + + A.CallTo(() => appWithoutTeam.Clients) + .Returns(AppClients.Empty.Add(clientId, clientId).Update(clientId, apiCallsLimit: 1000)); + + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A._, ct)) + .Returns(1000); + + var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + + Assert.True(isBlocked); + + A.CallTo(() => messaging.PublishAsync(A._, null, ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_true_if_over_blocking_limit() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 600, MaxApiCalls = 600 }; + + A.CallTo(() => billingPlans.GetActualPlan(A._)) + .ReturnsLazily(x => (plan, plan.Id)); + + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A._, ct)) + .Returns(1000); + + var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + + Assert.True(isBlocked); + + A.CallTo(() => messaging.PublishAsync(A._, null, ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_false_if_below_blocking_limit() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 1600, MaxApiCalls = 1600 }; + + A.CallTo(() => billingPlans.GetActualPlan(A._)) + .ReturnsLazily(x => (plan, plan.Id)); + + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A._, ct)) + .Returns(100); + + var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + + Assert.False(isBlocked); + + A.CallTo(() => messaging.PublishAsync(A._, null, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_return_false_and_notify_if_about_to_over_included_contingent() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 3000 }; + + A.CallTo(() => billingPlans.GetActualPlan(A._)) + .ReturnsLazily(x => (plan, plan.Id)); + + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A._, ct)) + .Returns(1200); // in 10 days = 4000 / month + + var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + + Assert.False(isBlocked); + + A.CallTo(() => messaging.PublishAsync(A._, null, ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_false_and_notify_if_about_to_over_included_contingent_but_no_max_given() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 0 }; + + A.CallTo(() => billingPlans.GetActualPlan(A._)) + .ReturnsLazily(x => (plan, plan.Id)); + + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A._, ct)) + .Returns(1200); // in 10 days = 4000 / month + + var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + + Assert.False(isBlocked); + + A.CallTo(() => messaging.PublishAsync(A._, null, ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_only_notify_once_if_about_to_be_over_included_contingent() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 3000 }; + + A.CallTo(() => billingPlans.GetActualPlan(A._)) + .ReturnsLazily(x => (plan, plan.Id)); + + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A._, ct)) + .Returns(1200); // in 10 days = 4000 / month + + await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + + A.CallTo(() => messaging.PublishAsync(A._, null, ct)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Should_not_notify_if_lower_than_10_percent() + { + var now = new DateTime(2020, 10, 2); + + var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 3000 }; + + A.CallTo(() => billingPlans.GetActualPlan(A._)) + .ReturnsLazily(x => (plan, plan.Id)); + + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), now, A._, ct)) + .Returns(220); // in 3 days = 3300 / month + + await sut.IsBlockedAsync(appWithoutTeam, clientId, now, ct); + await sut.IsBlockedAsync(appWithoutTeam, clientId, now, ct); + + A.CallTo(() => messaging.PublishAsync(A._, null, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_get_app_asset_total_size_from_summary_date() + { + A.CallTo(() => usageTracker.GetAsync($"{appId.Id}_Assets", default, default, null, ct)) + .Returns(new Counters { ["TotalSize"] = 2048 }); + + var size = await sut.GetTotalSizeByAppAsync(appId.Id, ct); + + Assert.Equal(2048, size); + } + + [Fact] + public async Task Should_get_team_asset_total_size_from_summary_date() + { + A.CallTo(() => usageTracker.GetAsync($"{appId.Id}_TeamAssets", default, default, null, ct)) + .Returns(new Counters { ["TotalSize"] = 2048 }); + + var size = await sut.GetTotalSizeByTeamAsync(appId.Id, ct); + + Assert.Equal(2048, size); + } + + [Fact] + public async Task Should_track_request_async() + { + await sut.TrackRequestAsync(appWithoutTeam, "client", today, 42, 50, 512, ct); + + A.CallTo(() => apiUsageTracker.TrackAsync(today, appWithoutTeam.Id.ToString(), "client", 42, 50, 512, ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_track_request_for_team_async() + { + await sut.TrackRequestAsync(appWithTeam, "client", today, 42, 50, 512, ct); + + A.CallTo(() => apiUsageTracker.TrackAsync(today, appWithTeam.TeamId!.ToString()!, appWithTeam.Name, 42, 50, 512, ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_get_app_asset_counters_from_categories() + { + SetupAssetQuery($"{appId.Id}_Assets"); + + var actual = await sut.QueryByAppAsync(appId.Id, today, today.AddDays(3), ct); + + actual.Should().BeEquivalentTo(new List + { + new AssetStats(today.AddDays(0), 2, 128), + new AssetStats(today.AddDays(1), 3, 256), + new AssetStats(today.AddDays(2), 4, 512) + }); + } + + [Fact] + public async Task Should_get_team_asset_counters_from_categories() + { + SetupAssetQuery($"{appId.Id}_TeamAssets"); + + var actual = await sut.QueryByTeamAsync(appId.Id, today, today.AddDays(3), ct); + + actual.Should().BeEquivalentTo(new List + { + new AssetStats(today.AddDays(0), 2, 128), + new AssetStats(today.AddDays(1), 3, 256), + new AssetStats(today.AddDays(2), 4, 512) + }); + } + + private void SetupAssetQuery(string key) + { + A.CallTo(() => usageTracker.QueryAsync(key, today, today.AddDays(3), ct)) + .Returns(new Dictionary> + { + [usageTracker.FallbackCategory] = new List<(DateTime, Counters)> + { + (today.AddDays(0), new Counters + { + ["TotalSize"] = 128, + ["TotalAssets"] = 2 + }), + (today.AddDays(1), new Counters + { + ["TotalSize"] = 256, + ["TotalAssets"] = 3 + }), + (today.AddDays(2), new Counters + { + ["TotalSize"] = 512, + ["TotalAssets"] = 4 + }) + } + }); + } + + [Fact] + public async Task Should_increase_usage_for_asset_event() + { + Counters? countersSummary = null; + Counters? countersDate = null; + + A.CallTo(() => usageTracker.TrackAsync(default, $"{appId.Id}_Assets", null, A._, ct)) + .Invokes(x => countersSummary = x.GetArgument(3)); + + A.CallTo(() => usageTracker.TrackAsync(today, $"{appId.Id}_Assets", null, A._, ct)) + .Invokes(x => countersDate = x.GetArgument(3)); + + await sut.TrackAssetAsync(appWithoutTeam.Id, today, 512, 3, ct); + + var expected = new Counters + { + ["TotalSize"] = 512, + ["TotalAssets"] = 3 + }; + + countersSummary.Should().BeEquivalentTo(expected); + countersDate.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_increase_team_usage_for_asset_event_and_team_app() + { + Counters? countersSummary = null; + Counters? countersDate = null; + + var team = A.Fake(); + + A.CallTo(() => team.Id) + .Returns(teamId); + + A.CallTo(() => appProvider.GetAppAsync(appWithTeam.Id, true, ct)) + .Returns(appWithTeam); + + A.CallTo(() => appProvider.GetTeamAsync(teamId, ct)) + .Returns(team); + + A.CallTo(() => usageTracker.TrackAsync(default, $"{teamId}_TeamAssets", null, A._, ct)) + .Invokes(x => countersSummary = x.GetArgument(3)); + + A.CallTo(() => usageTracker.TrackAsync(today, $"{teamId}_TeamAssets", null, A._, ct)) + .Invokes(x => countersDate = x.GetArgument(3)); + + await sut.TrackAssetAsync(appWithTeam.Id, today, 512, 3, ct); + + var expected = new Counters + { + ["TotalSize"] = 512, + ["TotalAssets"] = 3 + }; + + countersSummary.Should().BeEquivalentTo(expected); + countersDate.Should().BeEquivalentTo(expected); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierWorkerTest.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageNotifierWorkerTest.cs similarity index 99% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierWorkerTest.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageNotifierWorkerTest.cs index 0ff28d58a..9a856fd57 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierWorkerTest.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageNotifierWorkerTest.cs @@ -13,7 +13,7 @@ using Squidex.Infrastructure.TestHelpers; using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps.Plans +namespace Squidex.Domain.Apps.Entities.Billing { public class UsageNotifierWorkerTest { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs index 82b6b21c6..1fe44808b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs @@ -82,12 +82,12 @@ namespace Squidex.Domain.Apps.Entities.Comments A.CallTo(() => userResolver.QueryManyAsync(userIds, default)) .Returns(users.ToDictionary(x => x.Id)); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - Assert.Equal(2, result.Count); + Assert.Equal(2, actual.Count); - var enrichedEvent1 = result[0] as EnrichedCommentEvent; - var enrichedEvent2 = result[1] as EnrichedCommentEvent; + var enrichedEvent1 = actual[0] as EnrichedCommentEvent; + var enrichedEvent2 = actual[1] as EnrichedCommentEvent; Assert.Equal(user1, enrichedEvent1!.MentionedUser); Assert.Equal(user2, enrichedEvent2!.MentionedUser); @@ -109,9 +109,9 @@ namespace Squidex.Domain.Apps.Entities.Comments var @event = new CommentCreated { Mentions = userIds }; var envelope = Envelope.Create(@event); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -122,9 +122,9 @@ namespace Squidex.Domain.Apps.Entities.Comments var @event = new CommentCreated { Mentions = null }; var envelope = Envelope.Create(@event); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - Assert.Empty(result); + Assert.Empty(actual); A.CallTo(() => userResolver.QueryManyAsync(A._, A._)) .MustNotHaveHappened(); @@ -138,9 +138,9 @@ namespace Squidex.Domain.Apps.Entities.Comments var @event = new CommentCreated { Mentions = Array.Empty() }; var envelope = Envelope.Create(@event); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - Assert.Empty(result); + Assert.Empty(actual); A.CallTo(() => userResolver.QueryManyAsync(A._, A._)) .MustNotHaveHappened(); @@ -153,9 +153,9 @@ namespace Squidex.Domain.Apps.Entities.Comments { var @event = new CommentCreated(); - var result = sut.Trigger(Envelope.Create(@event), ctx); + var actual = sut.Trigger(Envelope.Create(@event), ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -166,9 +166,9 @@ namespace Squidex.Domain.Apps.Entities.Comments { var @event = new EnrichedCommentEvent(); - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -179,9 +179,9 @@ namespace Squidex.Domain.Apps.Entities.Comments { var @event = new EnrichedCommentEvent(); - var result = sut.Trigger(new EnrichedCommentEvent(), ctx); + var actual = sut.Trigger(new EnrichedCommentEvent(), ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -192,9 +192,9 @@ namespace Squidex.Domain.Apps.Entities.Comments { var @event = new EnrichedCommentEvent(); - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.False(result); + Assert.False(actual); }); } @@ -208,9 +208,9 @@ namespace Squidex.Domain.Apps.Entities.Comments MentionedUser = UserMocks.User("1", "1@email.com") }; - var result = handler.Trigger(@event, ctx); + var actual = handler.Trigger(@event, ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -224,9 +224,9 @@ namespace Squidex.Domain.Apps.Entities.Comments MentionedUser = UserMocks.User("1", "1@email.com") }; - var result = handler.Trigger(@event, ctx); + var actual = handler.Trigger(@event, ctx); - Assert.False(result); + Assert.False(actual); }); } @@ -240,9 +240,9 @@ namespace Squidex.Domain.Apps.Entities.Comments Text = "very_urgent_text" }; - var result = handler.Trigger(@event, ctx); + var actual = handler.Trigger(@event, ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -256,9 +256,9 @@ namespace Squidex.Domain.Apps.Entities.Comments Text = "just_gossip" }; - var result = handler.Trigger(@event, ctx); + var actual = handler.Trigger(@event, ctx); - Assert.False(result); + Assert.False(actual); }); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs index 1ec792833..abb66f6fe 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs @@ -41,9 +41,9 @@ namespace Squidex.Domain.Apps.Entities.Comments A.CallTo(() => domainObject.GetComments(11)) .Returns(comments); - var result = await sut.GetCommentsAsync(commentsId, 11, ct); + var actual = await sut.GetCommentsAsync(commentsId, 11, ct); - Assert.Same(comments, result); + Assert.Same(comments, actual); A.CallTo(() => domainObject.LoadAsync(ct)) .MustHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs index 1baa7c741..ae2657b2e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs @@ -150,7 +150,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject .Returns(user); } - private T CreateCommentsCommand(T command) where T : CommentsCommand + private T CreateCommentsCommand(T command) where T : CommentCommand { command.Actor = actor; command.AppId = appId; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs index 53a9fc77b..0d5f4bbbb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs @@ -43,9 +43,9 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { var command = new CreateComment { Text = "text1", Url = new Uri("http://uri") }; - var result = await sut.ExecuteAsync(CreateCommentsCommand(command), default); + var actual = await sut.ExecuteAsync(CreateCommentsCommand(command), default); - result.ShouldBeEquivalent(CommandResult.Empty(commentsId, 0, EtagVersion.Empty)); + actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 0, EtagVersion.Empty)); sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult { @@ -74,9 +74,9 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject var updateCommand = new UpdateComment { Text = "text2" }; - var result = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand), default); + var actual = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand), default); - result.ShouldBeEquivalent(CommandResult.Empty(commentsId, 1, 0)); + actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 1, 0)); sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult { @@ -110,9 +110,9 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject var deleteCommand = new DeleteComment(); - var result = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand), default); + var actual = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand), default); - result.ShouldBeEquivalent(CommandResult.Empty(commentsId, 2, 1)); + actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 2, 1)); sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult { @@ -162,7 +162,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject return @event; } - private T CreateCommentsCommand(T command) where T : CommentsCommand + private T CreateCommentsCommand(T command) where T : CommentCommand { command.Actor = actor; command.CommentsId = commentsId; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs index 7b4101e53..a2f966421 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs @@ -200,8 +200,8 @@ namespace Squidex.Domain.Apps.Entities.Contents Assert.Equal(new HashSet { - DomainId.Combine(appId.Id, contentId1), - DomainId.Combine(appId.Id, contentId2) + DomainId.Combine(appId, contentId1), + DomainId.Combine(appId, contentId2) }, rebuildContents); } @@ -209,7 +209,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { @event.AppId = appId; - return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId.Id, @event.ContentId)); + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.ContentId)); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index 907f994d1..52e9f7071 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -137,9 +137,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new ContentEntity { SchemaId = schemaNonMatch } }.ToAsyncEnumerable()); - var result = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); + var actual = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); - var typed = result.OfType().ToList(); + var typed = actual.OfType().ToList(); Assert.Equal(2, typed.Count); Assert.Equal(2, typed.Count(x => x.Type == EnrichedContentEventType.Created)); @@ -169,9 +169,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new ContentEntity { SchemaId = schemaMatch } }.ToAsyncEnumerable()); - var result = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); + var actual = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); - var typed = result.OfType().ToList(); + var typed = actual.OfType().ToList(); Assert.Equal(2, typed.Count); Assert.Equal(2, typed.Count(x => x.Type == EnrichedContentEventType.Created)); @@ -191,9 +191,9 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12, ct)) .Returns(SimpleMapper.Map(@event, new ContentEntity())); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); - var enrichedEvent = (EnrichedContentEvent)result.Single(); + var enrichedEvent = (EnrichedContentEvent)actual.Single(); Assert.Equal(type, enrichedEvent!.Type); Assert.Equal(@event.Actor, enrichedEvent.Actor); @@ -221,9 +221,9 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 11, ct)) .Returns(new ContentEntity { AppId = ctx.AppId, SchemaId = schemaMatch, Version = 11, Data = dataOld }); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); - var enrichedEvent = result.Single() as EnrichedContentEvent; + var enrichedEvent = actual.Single() as EnrichedContentEvent; Assert.Same(dataNow, enrichedEvent!.Data); Assert.Same(dataOld, enrichedEvent!.DataOld); @@ -236,9 +236,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var @event = new ContentCreated { SchemaId = schemaMatch }; - var result = sut.Trigger(Envelope.Create(@event), ctx); + var actual = sut.Trigger(Envelope.Create(@event), ctx); - Assert.False(result); + Assert.False(actual); }); } @@ -249,9 +249,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var @event = new ContentCreated { SchemaId = schemaMatch }; - var result = sut.Trigger(Envelope.Create(@event), ctx); + var actual = sut.Trigger(Envelope.Create(@event), ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -262,9 +262,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var @event = new ContentCreated { SchemaId = schemaMatch }; - var result = sut.Trigger(Envelope.Create(@event), ctx); + var actual = sut.Trigger(Envelope.Create(@event), ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -275,9 +275,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var @event = new ContentCreated { SchemaId = schemaMatch }; - var result = sut.Trigger(Envelope.Create(@event), ctx); + var actual = sut.Trigger(Envelope.Create(@event), ctx); - Assert.False(result); + Assert.False(actual); }); } @@ -288,9 +288,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.False(result); + Assert.False(actual); }); } @@ -301,9 +301,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -314,9 +314,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -327,9 +327,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -340,9 +340,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.False(result); + Assert.False(actual); }); } @@ -353,9 +353,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.False(result); + Assert.False(actual); }); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs index 10c9da53f..b5e3b1c87 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs @@ -157,9 +157,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var ctx = ContextWithPermissions(); - var result = await sut.SearchAsync("query", ctx, ct); + var actual = await sut.SearchAsync("query", ctx, ct); - Assert.Empty(result); + Assert.Empty(actual); A.CallTo(() => contentIndex.SearchAsync(ctx.App, A._, A._, A._)) .MustNotHaveHappened(); @@ -173,9 +173,9 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentIndex.SearchAsync(ctx.App, A.That.Matches(x => x.Text == "query~"), ctx.Scope(), ct)) .Returns(new List()); - var result = await sut.SearchAsync("query", ctx, ct); + var actual = await sut.SearchAsync("query", ctx, ct); - Assert.Empty(result); + Assert.Empty(actual); A.CallTo(() => contentQuery.QueryAsync(ctx, A._, A._)) .MustNotHaveHappened(); @@ -198,9 +198,9 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => urlGenerator.ContentUI(appId, schemaId1, content.Id)) .Returns("content-url"); - var result = await sut.SearchAsync("query", ctx, ct); + var actual = await sut.SearchAsync("query", ctx, ct); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add(expectedName, SearchResultType.Content, "content-url")); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs index cc47c9893..5aa018b59 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs @@ -51,9 +51,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter ["appId"] = appId }; - var result = sut.Execute(vars, script).ToString(); + var actual = sut.Execute(vars, script).ToString(); - Assert.Equal("3", result); + Assert.Equal("3", actual); } [Fact] @@ -65,8 +65,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter .Returns(3); const string script = @" - resetCounterV2('my', function(result) { - complete(result); + resetCounterV2('my', function(actual) { + complete(actual); }, 4); "; @@ -75,9 +75,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter ["appId"] = appId }; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal("3", result); + Assert.Equal("3", actual); } [Fact] @@ -97,9 +97,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter ["appId"] = appId }; - var result = sut.Execute(vars, script).ToString(); + var actual = sut.Execute(vars, script).ToString(); - Assert.Equal("3", result); + Assert.Equal("3", actual); } [Fact] @@ -111,8 +111,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter .Returns(3); const string script = @" - incrementCounter('my', function (result) { - complete(result); + incrementCounter('my', function (actual) { + complete(actual); }); "; @@ -121,9 +121,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter ["appId"] = appId }; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal("3", result); + Assert.Equal("3", actual); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs index 006b91940..674750be0 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -38,17 +38,17 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_draft_as_initial_status() { - var result = await sut.GetInitialStatusAsync(null!); + var actual = await sut.GetInitialStatusAsync(null!); - Assert.Equal(Status.Draft, result); + Assert.Equal(Status.Draft, actual); } [Fact] public async Task Should_allow_publish_on_create() { - var result = await sut.CanPublishInitialAsync(null!, null); + var actual = await sut.CanPublishInitialAsync(null!, null); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -56,9 +56,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = new ContentEntity { Status = Status.Published }; - var result = await sut.CanMoveToAsync(null!, content.Status, Status.Draft, null!, null!); + var actual = await sut.CanMoveToAsync(null!, content.Status, Status.Draft, null!, null!); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -66,9 +66,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = new ContentEntity { Status = Status.Published }; - var result = await sut.CanMoveToAsync(content, content.Status, Status.Draft, null!); + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Draft, null!); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -76,9 +76,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = new ContentEntity { Status = Status.Published }; - var result = await sut.CanUpdateAsync(content, content.Status, null!); + var actual = await sut.CanUpdateAsync(content, content.Status, null!); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -86,9 +86,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = new ContentEntity { Status = Status.Published }; - var result = await sut.CanUpdateAsync(content, content.Status, null!); + var actual = await sut.CanUpdateAsync(content, content.Status, null!); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -96,9 +96,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = new ContentEntity { Status = Status.Archived }; - var result = await sut.CanUpdateAsync(content, content.Status, null!); + var actual = await sut.CanUpdateAsync(content, content.Status, null!); - Assert.False(result); + Assert.False(actual); } [Fact] @@ -112,9 +112,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Published, StatusColors.Published) }; - var result = await sut.GetNextAsync(content, content.Status, null!); + var actual = await sut.GetNextAsync(content, content.Status, null!); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -127,9 +127,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Draft, StatusColors.Draft) }; - var result = await sut.GetNextAsync(content, content.Status, null!); + var actual = await sut.GetNextAsync(content, content.Status, null!); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -143,9 +143,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Draft, StatusColors.Draft) }; - var result = await sut.GetNextAsync(content, content.Status, null!); + var actual = await sut.GetNextAsync(content, content.Status, null!); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -158,33 +158,33 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Published, StatusColors.Published) }; - var result = await sut.GetAllAsync(null!); + var actual = await sut.GetAllAsync(null!); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] public async Task Should_not_validate_when_not_publishing() { - var result = await sut.ShouldValidateAsync(null!, Status.Draft); + var actual = await sut.ShouldValidateAsync(null!, Status.Draft); - Assert.False(result); + Assert.False(actual); } [Fact] public async Task Should_not_validate_when_publishing_but_not_enabled() { - var result = await sut.ShouldValidateAsync(CreateSchema(false), Status.Published); + var actual = await sut.ShouldValidateAsync(CreateSchema(false), Status.Published); - Assert.False(result); + Assert.False(actual); } [Fact] public async Task Should_validate_when_publishing_and_enabled() { - var result = await sut.ShouldValidateAsync(CreateSchema(true), Status.Published); + var actual = await sut.ShouldValidateAsync(CreateSchema(true), Status.Published); - Assert.True(result); + Assert.True(actual); } private static ISchemaEntity CreateSchema(bool validateOnPublish) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs index 539448a2c..989bf6389 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs @@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } [Fact] - public async Task Should_not_invoke_enricher_for_other_result() + public async Task Should_not_invoke_enricher_for_other_actual() { await HandleAsync(new CreateContent(), 12); @@ -60,36 +60,36 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject [Fact] public async Task Should_not_invoke_enricher_if_already_enriched() { - var result = new ContentEntity(); + var actual = new ContentEntity(); var context = await HandleAsync(new CreateContent(), - result); + actual); - Assert.Same(result, context.Result()); + Assert.Same(actual, context.Result()); A.CallTo(() => contentEnricher.EnrichAsync(A._, A._, requestContext, A._)) .MustNotHaveHappened(); } [Fact] - public async Task Should_enrich_content_result() + public async Task Should_enrich_content_actual() { - var result = A.Fake(); + var actual = A.Fake(); var enriched = new ContentEntity(); - A.CallTo(() => contentEnricher.EnrichAsync(result, true, requestContext, A._)) + A.CallTo(() => contentEnricher.EnrichAsync(actual, true, requestContext, A._)) .Returns(enriched); var context = await HandleAsync(new CreateContent(), - result); + actual); Assert.Same(enriched, context.Result()); } - private Task HandleAsync(ContentCommand command, object result) + private Task HandleAsync(ContentCommand command, object actual) { command.ContentId = contentId; @@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var domainObject = A.Fake(); A.CallTo(() => domainObject.ExecuteAsync(A._, A._)) - .Returns(new CommandResult(command.AggregateId, 1, 0, result)); + .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); A.CallTo(() => domainObjectFactory.Create(command.AggregateId)) .Returns(domainObject); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs index 7f3d26940..3304dac4b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs @@ -138,9 +138,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { var command = new CreateContent { Data = data }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); @@ -161,9 +161,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { var command = new CreateContent { Data = data, Status = Status.Draft }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); @@ -184,9 +184,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { var command = new CreateContent { Data = data, Status = Status.Archived }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); Assert.Equal(Status.Archived, sut.Snapshot.Status); @@ -238,9 +238,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { var command = new UpsertContent { Data = data }; - var result = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); Assert.Equal(Status.Draft, sut.Snapshot.Status); @@ -261,9 +261,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { var command = new UpsertContent { Data = data }; - var result = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); Assert.Equal(Status.Draft, sut.Snapshot.Status); @@ -284,9 +284,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { var command = new UpsertContent { Data = data, Status = Status.Archived }; - var result = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); Assert.Equal(Status.Archived, sut.Snapshot.Status); @@ -310,9 +310,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); @@ -332,9 +332,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(patched, sut.Snapshot.CurrentVersion.Data); @@ -354,9 +354,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); @@ -376,9 +376,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); Assert.Equal(Status.Archived, sut.Snapshot.Status); @@ -434,9 +434,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); @@ -458,9 +458,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecutePublishAsync(); await ExecuteCreateDraftAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(otherData, sut.Snapshot.NewVersion?.Data); @@ -480,9 +480,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Single(LastEvents); @@ -507,9 +507,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.NotEqual(data, sut.Snapshot.CurrentVersion.Data); @@ -531,9 +531,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecutePublishAsync(); await ExecuteCreateDraftAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(patched, sut.Snapshot.NewVersion?.Data); @@ -553,9 +553,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Single(LastEvents); @@ -570,9 +570,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Archived, sut.Snapshot.CurrentVersion.Status); @@ -592,9 +592,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Archived, sut.Snapshot.CurrentVersion.Status); @@ -615,9 +615,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); await ExecutePublishAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); @@ -641,9 +641,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); await ExecutePublishAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); @@ -667,9 +667,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecutePublishAsync(); await ExecuteCreateDraftAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Archived, sut.Snapshot.NewVersion?.Status); @@ -691,9 +691,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecutePublishAsync(); await ExecuteCreateDraftAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Null(sut.Snapshot.NewVersion?.Status); @@ -715,9 +715,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); Assert.Equal(Status.Published, sut.Snapshot.ScheduleJob?.Status); @@ -746,9 +746,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject A.CallTo(() => contentWorkflow.CanMoveToAsync(A._, Status.Draft, Status.Archived, User)) .Returns(true); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Null(sut.Snapshot.ScheduleJob); @@ -774,9 +774,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject A.CallTo(() => contentWorkflow.CanMoveToAsync(A._, Status.Draft, Status.Published, User)) .Returns(false); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Null(sut.Snapshot.ScheduleJob); @@ -825,9 +825,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); await ExecuteChangeStatusAsync(Status.Published, SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(1))); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Null(sut.Snapshot.ScheduleJob); @@ -856,9 +856,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = new DeleteContent(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); Assert.True(sut.Snapshot.IsDeleted); @@ -878,9 +878,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = new DeleteContent { Permanent = true }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version); Assert.Empty(LastEvents); @@ -923,9 +923,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecuteCreateAsync(); await ExecutePublishAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.NewVersion?.Status); @@ -944,9 +944,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject await ExecutePublishAsync(); await ExecuteCreateDraftAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Null(sut.Snapshot.NewVersion); @@ -1028,9 +1028,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject private async Task PublishAsync(ContentCommand command) { - var result = await sut.ExecuteAsync(CreateContentCommand(command), default); + var actual = await sut.ExecuteAsync(CreateContentCommand(command), default); - return result.Payload; + return actual.Payload; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs index c0cf7e14d..61857cb49 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs @@ -49,9 +49,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { var command = new BulkUpdateContents(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -59,9 +59,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { var command = new BulkUpdateContents { Jobs = Array.Empty() }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -73,10 +73,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.ChangeStatus); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainObjectNotFoundException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainObjectNotFoundException); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -99,10 +99,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.ChangeStatus, new BulkUpdateJob { Query = query }); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainException); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -125,10 +125,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Query = query, Data = data }); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.Data == data && x.ContentId == id), ct)) @@ -159,11 +159,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject command.Jobs![0].ExpectedCount = 2; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Equal(2, result.Count); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id1 && x.Exception == null); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id2 && x.Exception == null); + Assert.Equal(2, actual.Count); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id1 && x.Exception == null); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id2 && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.Data == data && x.ContentId == id1), ct)) @@ -183,10 +183,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Data = data }); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.Data == data && x.ContentId != default), ct)) @@ -194,7 +194,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } [Fact] - public async Task Should_upsert_content_with_random_id_if_query_returns_no_result() + public async Task Should_upsert_content_with_random_id_if_query_returns_no_actual() { SetupContext(PermissionIds.AppContentsUpsert); @@ -202,10 +202,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Query = query, Data = data }); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.Data == data && x.ContentId != default), ct)) @@ -221,10 +221,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Data = data }, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.Data == data && x.ContentId == id), ct)) @@ -240,10 +240,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Data = data }, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.Data == data && x.ContentId == id), ct)) @@ -259,10 +259,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Create, new BulkUpdateJob { Data = data }, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id && x.Data == data), ct)) @@ -278,10 +278,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Create, new BulkUpdateJob { Data = data }, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -296,10 +296,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Update, new BulkUpdateJob { Data = data }, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id && x.Data == data), ct)) @@ -315,10 +315,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Update, new BulkUpdateJob { Data = data }, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -333,10 +333,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Patch, new BulkUpdateJob { Data = data }, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id && x.Data == data), ct)) @@ -352,10 +352,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Delete, new BulkUpdateJob { Data = data }, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -370,10 +370,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.ChangeStatus, id: id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id && x.DueTime == null), ct)) @@ -389,10 +389,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.ChangeStatus, new BulkUpdateJob { DueTime = time }, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id && x.DueTime == time), ct)) @@ -408,10 +408,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.ChangeStatus, id: id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -426,10 +426,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Validate, id: id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id), ct)) @@ -445,10 +445,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Validate, id: id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -463,10 +463,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Delete, id: id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == id), ct)) @@ -482,10 +482,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Delete, id: id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -503,10 +503,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject var command = BulkCommand(BulkUpdateContentType.Delete, new BulkUpdateJob { Schema = schemaCustomId.Name }, id); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(result); - Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.SchemaId == schemaCustomId), ct)) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs index d2a850bc6..a90c5fb74 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -125,25 +125,25 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_draft_as_initial_status() { - var result = await sut.GetInitialStatusAsync(Mocks.Schema(appId, schemaId)); + var actual = await sut.GetInitialStatusAsync(Mocks.Schema(appId, schemaId)); - Assert.Equal(Status.Draft, result); + Assert.Equal(Status.Draft, actual); } [Fact] public async Task Should_allow_publish_on_create() { - var result = await sut.CanPublishInitialAsync(Mocks.Schema(appId, schemaId), Mocks.FrontendUser("Editor")); + var actual = await sut.CanPublishInitialAsync(Mocks.Schema(appId, schemaId), Mocks.FrontendUser("Editor")); - Assert.True(result); + Assert.True(actual); } [Fact] public async Task Should_not_allow_publish_on_create_if_role_not_allowed() { - var result = await sut.CanPublishInitialAsync(Mocks.Schema(appId, schemaId), Mocks.FrontendUser("Developer")); + var actual = await sut.CanPublishInitialAsync(Mocks.Schema(appId, schemaId), Mocks.FrontendUser("Developer")); - Assert.False(result); + Assert.False(actual); } [Fact] @@ -151,9 +151,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Draft, 2); - var result = await sut.CanMoveToAsync(Mocks.Schema(appId, schemaId), content.Status, Status.Published, content.Data, Mocks.FrontendUser("Editor")); + var actual = await sut.CanMoveToAsync(Mocks.Schema(appId, schemaId), content.Status, Status.Published, content.Data, Mocks.FrontendUser("Editor")); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -161,9 +161,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Draft, 2); - var result = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -171,9 +171,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Draft, 2); - var result = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Developer")); + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Developer")); - Assert.False(result); + Assert.False(actual); } [Fact] @@ -181,9 +181,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Draft, 2); - var result = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -191,9 +191,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Draft, 4); - var result = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); - Assert.False(result); + Assert.False(actual); } [Fact] @@ -201,9 +201,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Published, 2); - var result = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -211,9 +211,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Published, 2); - var result = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -221,9 +221,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Archived, 2); - var result = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); - Assert.False(result); + Assert.False(actual); } [Fact] @@ -231,9 +231,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Published, 2); - var result = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); - Assert.False(result); + Assert.False(actual); } [Fact] @@ -241,9 +241,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Published, 1); - var result = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -251,9 +251,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Published, 2); - var result = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Editor")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Editor")); - Assert.False(result); + Assert.False(actual); } [Fact] @@ -261,9 +261,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = CreateContent(Status.Published, 1); - var result = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -276,9 +276,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Archived, StatusColors.Archived) }; - var result = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Developer")); + var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Developer")); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -291,9 +291,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Archived, StatusColors.Archived) }; - var result = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Editor")); + var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Editor")); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -307,9 +307,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Published, StatusColors.Published) }; - var result = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Editor")); + var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Editor")); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -322,9 +322,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Draft, StatusColors.Draft) }; - var result = await sut.GetNextAsync(content, content.Status, null!); + var actual = await sut.GetNextAsync(content, content.Status, null!); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -338,9 +338,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Draft, StatusColors.Draft) }; - var result = await sut.GetNextAsync(content, content.Status, null!); + var actual = await sut.GetNextAsync(content, content.Status, null!); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -353,9 +353,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Published, StatusColors.Published) }; - var result = await sut.GetAllAsync(Mocks.Schema(appId, schemaId)); + var actual = await sut.GetAllAsync(Mocks.Schema(appId, schemaId)); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -367,9 +367,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Published, StatusColors.Published) }; - var result = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); + var actual = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -384,41 +384,41 @@ namespace Squidex.Domain.Apps.Entities.Contents new StatusInfo(Status.Published, StatusColors.Published) }; - var result = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); + var actual = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] public async Task Should_not_validate_when_not_publishing() { - var result = await sut.ShouldValidateAsync(Mocks.Schema(appId, schemaId), Status.Draft); + var actual = await sut.ShouldValidateAsync(Mocks.Schema(appId, schemaId), Status.Draft); - Assert.False(result); + Assert.False(actual); } [Fact] public async Task Should_not_validate_when_publishing_but_not_enabled() { - var result = await sut.ShouldValidateAsync(CreateSchema(false), Status.Published); + var actual = await sut.ShouldValidateAsync(CreateSchema(false), Status.Published); - Assert.False(result); + Assert.False(actual); } [Fact] public async Task Should_validate_when_publishing_and_enabled() { - var result = await sut.ShouldValidateAsync(CreateSchema(true), Status.Published); + var actual = await sut.ShouldValidateAsync(CreateSchema(true), Status.Published); - Assert.True(result); + Assert.True(actual); } [Fact] public async Task Should_validate_when_enabled_in_step() { - var result = await sut.ShouldValidateAsync(Mocks.Schema(appId, schemaId), Status.Archived); + var actual = await sut.ShouldValidateAsync(Mocks.Schema(appId, schemaId), Status.Archived); - Assert.True(result); + Assert.True(actual); } private ISchemaEntity CreateSchema(bool validateOnPublish) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs index 239f75eb9..90779f706 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs @@ -110,9 +110,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }"; - var result = await ExecuteAsync(new ExecutionOptions { Query = query, OperationName = "IntrospectionQuery" }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query, OperationName = "IntrospectionQuery" }); - var json = serializer.Serialize(result); + var json = serializer.Serialize(actual); Assert.NotEmpty(json); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs index e26d96894..a77ad1b73 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }"; - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL data = (object?)null }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsCreate; - var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); var expected = new { @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsCreate; - var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); var expected = new { @@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -162,7 +162,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsCreate; - var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); var expected = new { @@ -172,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -194,7 +194,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }", contentId, content); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -220,7 +220,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL data = (object?)null }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -240,7 +240,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsUpdateOwn; - var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); var expected = new { @@ -250,7 +250,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -276,7 +276,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsUpdateOwn; - var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); var expected = new { @@ -286,7 +286,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -308,7 +308,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }", contentId, content); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -334,7 +334,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL data = (object?)null }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -354,7 +354,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsUpsert; - var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); var expected = new { @@ -364,7 +364,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -391,7 +391,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsUpsert; - var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); var expected = new { @@ -401,7 +401,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -424,7 +424,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }", contentId, content); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -450,7 +450,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL data = (object?)null }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -470,7 +470,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsUpdateOwn; - var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); var expected = new { @@ -480,7 +480,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -506,7 +506,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsUpdateOwn; - var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); var expected = new { @@ -516,7 +516,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -538,7 +538,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }", contentId, content); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -564,7 +564,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL data = (object?)null }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -586,7 +586,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsChangeStatusOwn; - var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); var expected = new { @@ -596,7 +596,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -623,7 +623,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsChangeStatusOwn; - var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); var expected = new { @@ -633,7 +633,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -660,7 +660,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsChangeStatusOwn; - var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); var expected = new { @@ -670,7 +670,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => @@ -693,7 +693,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }", contentId, content); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -719,7 +719,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL data = (object?)null }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); @@ -739,7 +739,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.AppContentsDeleteOwn; - var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); var expected = new { @@ -752,7 +752,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index c62818625..b01bdc17b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [InlineData(" ")] public async Task Should_return_error_empty_query(string query) { - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal), A._)) .Returns(ResultList.CreateFrom(0, asset)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -97,7 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && !x.NoTotal), A._)) .Returns(ResultList.CreateFrom(10, asset)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -114,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(assetId), A._)) .Returns(ResultList.CreateFrom(1)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -143,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -163,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(assetId), A._)) .Returns(ResultList.CreateFrom(1, asset)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -173,7 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -193,7 +193,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A._)) .Returns(ResultList.CreateFrom(0, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -206,7 +206,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -226,7 +226,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A._)) .Returns(ResultList.CreateFrom(0, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -239,7 +239,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -262,7 +262,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && !x.NoTotal), A._)) .Returns(ResultList.CreateFrom(10, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -279,7 +279,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -298,7 +298,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(contentId), A._)) .Returns(ResultList.CreateFrom(1)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -308,7 +308,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -328,7 +328,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(contentId), A._)) .Returns(ResultList.CreateFrom(10, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -338,7 +338,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -358,7 +358,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(contentId), A._)) .Returns(ResultList.CreateFrom(1, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -368,7 +368,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -387,7 +387,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => contentQuery.FindAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), contentId, 3, A._)) .Returns(content); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -397,7 +397,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -443,7 +443,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(contentId), A._)) .Returns(ResultList.CreateFrom(1, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -480,7 +480,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -519,7 +519,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(contentId), A._)) .Returns(ResultList.CreateFrom(1, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -552,7 +552,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -584,7 +584,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(contentId), A._)) .Returns(ResultList.CreateFrom(1, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -607,7 +607,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -639,8 +639,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(contentId), A._)) .Returns(ResultList.CreateFrom(1, content)); - var result1 = await ExecuteAsync(new ExecutionOptions { Query = query }); - var result2 = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual1 = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual2 = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -663,8 +663,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result1); - AssertResult(expected, result2); + AssertResult(expected, actual1); + AssertResult(expected, actual2); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentRefId), A._)) @@ -703,7 +703,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal), A._)) .Returns(ResultList.CreateFrom(1, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -730,7 +730,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -768,7 +768,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && !x.NoTotal), A._)) .Returns(ResultList.CreateFrom(10, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -799,7 +799,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -829,7 +829,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId && x.NoTotal), A._)) .Returns(ResultList.CreateFrom(1, contentRef)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -849,7 +849,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -882,7 +882,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId), A._)) .Returns(ResultList.CreateFrom(10, contentRef)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -906,7 +906,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -950,7 +950,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(contentId), A._)) .Returns(ResultList.CreateFrom(1, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -984,7 +984,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -1021,7 +1021,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(assetRefId), A._)) .Returns(ResultList.CreateFrom(0, assetRef)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -1051,7 +1051,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -1085,7 +1085,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(assetRefId), A._)) .Returns(ResultList.CreateFrom(0, assetRef)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -1111,7 +1111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -1142,9 +1142,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.HasIdsWithoutTotal(contentId), A._)) .Returns(ResultList.CreateFrom(1, content)); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var json = serializer.Serialize(result); + var json = serializer.Serialize(actual); Assert.Contains("\"errors\"", json, StringComparison.Ordinal); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs index 2622fb9c0..ce96db219 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs @@ -12,7 +12,6 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Subscriptions; using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; using Squidex.Shared; using Xunit; @@ -48,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, TestApp.Default.Name); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id); var expected = new { @@ -63,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -78,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }"); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -104,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL data = (object?)null }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -136,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var permission = PermissionIds.ForApp(PermissionIds.AppContentsRead, TestApp.Default.Name, "random-schema"); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id); var expected = new { @@ -156,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - AssertResult(expected, result); + AssertResult(expected, actual); } [Fact] @@ -170,7 +169,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }"); - var result = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -196,7 +195,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL data = (object?)null }; - AssertResult(expected, result); + AssertResult(expected, actual); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index 2a428f234..e773e57d3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -60,9 +60,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL requestContext = new Context(Mocks.FrontendUser(), TestApp.Default); } - protected void AssertResult(object expected, ExecutionResult result) + protected void AssertResult(object expected, ExecutionResult actual) { - var jsonOutputResult = serializer.Serialize(result); + var jsonOutputResult = serializer.Serialize(actual); var isonOutputExpected = serializer.Serialize(expected); Assert.Equal(isonOutputExpected, jsonOutputResult); @@ -97,20 +97,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL // Enrich the context with the schema. await sut.ExecuteAsync(options, x => Task.FromResult(null!)); - var result = await new DocumentExecuter().ExecuteAsync(options); + var actual = await new DocumentExecuter().ExecuteAsync(options); - if (result.Streams?.Count > 0 && result.Errors?.Any() != true) + if (actual.Streams?.Count > 0 && actual.Errors?.Any() != true) { - // Resolve the first stream result with a timeout. - var stream = result.Streams.First(); + // Resolve the first stream actual with a timeout. + var stream = actual.Streams.First(); using (var cts = new CancellationTokenSource(5000)) { - result = await stream.Value.FirstAsync().ToTask().WithCancellation(cts.Token); + actual = await stream.Value.FirstAsync().ToTask().WithCancellation(cts.Token); } } - return result; + return actual; } private static Context BuildContext(string permissionId) @@ -205,7 +205,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL x.App == TestApp.Default && x.ShouldSkipCleanup() && x.ShouldSkipContentEnrichment() && - x.User == requestContext.User); + x.UserPrincipal == requestContext.UserPrincipal); } protected Context MatchsContentContext() @@ -214,7 +214,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL x.App == TestApp.Default && x.ShouldSkipCleanup() && x.ShouldSkipContentEnrichment() && - x.User == requestContext.User); + x.UserPrincipal == requestContext.UserPrincipal); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs index b600a2de4..1610b97f1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs @@ -361,7 +361,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public static object Input(IContentEntity content, DomainId refId = default, DomainId assetId = default) { - var result = new Dictionary + var actual = new Dictionary { ["myJson"] = new { @@ -462,7 +462,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL if (refId != default) { - result["myReferences"] = new + actual["myReferences"] = new { iv = new[] { @@ -470,7 +470,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - result["myUnion"] = new + actual["myUnion"] = new { iv = new[] { @@ -481,7 +481,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL if (assetId != default) { - result["myAssets"] = new + actual["myAssets"] = new { iv = new[] { @@ -492,18 +492,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL if (assetId != default || refId != default) { - result["myEmbeds"] = new + actual["myEmbeds"] = new { iv = $"assets:{assetId}, contents:{refId}" }; } - return result; + return actual; } private static object Data(IContentEntity content) { - var result = new Dictionary + var actual = new Dictionary { ["myJson"] = new { @@ -629,12 +629,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - return result; + return actual; } private static object FlatData(IContentEntity content) { - var result = new Dictionary + var actual = new Dictionary { ["myJson"] = new { @@ -715,7 +715,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }; - return result; + return actual; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs index 5c864db38..7160583ba 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb private static ContentDomainObject.State CreateContentWithoutNewVersion() { - var user = new RefToken(RefTokenType.Subject, "1"); + var user = RefToken.User("1"); var data = new ContentData() @@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb private static ContentDomainObject.State CreateContentWithNewVersion() { - var user = new RefToken(RefTokenType.Subject, "1"); + var user = RefToken.User("1"); var data = new ContentData() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs index 8b3e261cb..8d0574b61 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs @@ -134,7 +134,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var now = SystemClock.Instance.GetCurrentInstant(); - var user = new RefToken(RefTokenType.Subject, "1"); + var user = RefToken.User("1"); foreach (var appId in AppIds) { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs index 13d79cf16..a4bd9af9c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs @@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var contents = await _.ContentRepository.QueryIdsAsync(_.RandomAppId(), _.RandomSchemaId(), filter); - // We have a concrete query, so we expect an result. + // We have a concrete query, so we expect an actual. Assert.NotEmpty(contents); } @@ -104,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var contents = await QueryAsync(_.ContentRepository, query, 1000, 0); - // We have a concrete query, so we expect an result. + // We have a concrete query, so we expect an actual. Assert.NotEmpty(contents); } @@ -126,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var contents = await QueryAsync(_.ContentRepository, query); - // We have a concrete query, so we expect an result. + // We have a concrete query, so we expect an actual. Assert.NotEmpty(contents); } @@ -154,7 +154,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var contents = await QueryAsync(_.ContentRepository, query, 1000, 9000); - // We have a concrete query, so we expect an result. + // We have a concrete query, so we expect an actual. Assert.NotEmpty(contents); } @@ -168,7 +168,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var contents = await QueryAsync(_.ContentRepository, query); - // The full text is resolved by another system, so we cannot verify the result. + // The full text is resolved by another system, so we cannot verify the actual. Assert.NotNull(contents); } @@ -182,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var contents = await QueryAsync(_.ContentRepository, query, 1000, 0); - // We have a concrete query, so we expect an result. + // We have a concrete query, so we expect an actual. Assert.NotEmpty(contents); } @@ -196,7 +196,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var contents = await QueryAsync(_.ContentRepository, query, 1000, 0, reference: DomainId.NewGuid()); - // We do not insert test entities with references, so we cannot verify the result. + // We do not insert test entities with references, so we cannot verify the actual. Assert.Empty(contents); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs index 32eb8bf84..e7fabe5e0 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs @@ -66,9 +66,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb using (var reader = new BsonBinaryReader(stream)) { - var result = BsonSerializer.Deserialize(reader); + var actual = BsonSerializer.Deserialize(reader); - return result; + return actual; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs index 4c066f6e5..3f6fe90b2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } [Fact] - public async Task Should_only_invoke_pre_enrich_for_empty_results() + public async Task Should_only_invoke_pre_enrich_for_empty_actuals() { var source = Array.Empty(); @@ -126,9 +126,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var sut = new ContentEnricher(Enumerable.Empty(), appProvider); - var result = await sut.EnrichAsync(source, true, requestContext, ct); + var actual = await sut.EnrichAsync(source, true, requestContext, ct); - Assert.NotSame(source.Data, result.Data); + Assert.NotSame(source.Data, actual.Data); } [Fact] @@ -138,9 +138,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var sut = new ContentEnricher(Enumerable.Empty(), appProvider); - var result = await sut.EnrichAsync(source, false, requestContext, ct); + var actual = await sut.EnrichAsync(source, false, requestContext, ct); - Assert.Same(source.Data, result.Data); + Assert.Same(source.Data, actual.Data); } private ContentEntity CreateContent(ContentData? data = null) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs index ef5bfc870..8c88cbe5e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs @@ -81,9 +81,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => domainObject.GetSnapshotAsync(EtagVersion.Any, ct)) .Returns(content); - var result = await sut.GetAsync(appId, id, EtagVersion.Any, ct); + var actual = await sut.GetAsync(appId, id, EtagVersion.Any, ct); - Assert.Same(content, result); + Assert.Same(content, actual); } [Fact] @@ -94,9 +94,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) .Returns(content); - var result = await sut.GetAsync(appId, id, 10, ct); + var actual = await sut.GetAsync(appId, id, 10, ct); - Assert.Same(content, result); + Assert.Same(content, actual); } [Fact] @@ -107,9 +107,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => domainObjectCache.GetAsync(DomainId.Combine(appId, id), 10, ct)) .Returns(content); - var result = await sut.GetAsync(appId, id, 10, ct); + var actual = await sut.GetAsync(appId, id, 10, ct); - Assert.Same(content, result); + Assert.Same(content, actual); A.CallTo(() => domainObjectFactory.Create(unqiueId)) .MustNotHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index 0e8cbc225..36fc5208b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -80,9 +80,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, true, ct)) .Returns(schema); - var result = await sut.GetSchemaOrThrowAsync(requestContext, input, ct); + var actual = await sut.GetSchemaOrThrowAsync(requestContext, input, ct); - Assert.Equal(schema, result); + Assert.Equal(schema, actual); } [Fact] @@ -95,9 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, true, ct)) .Returns(schema); - var result = await sut.GetSchemaOrThrowAsync(requestContext, input, ct); + var actual = await sut.GetSchemaOrThrowAsync(requestContext, input, ct); - Assert.Equal(schema, result); + Assert.Equal(schema, actual); } [Fact] @@ -134,9 +134,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A._, A._)) .Returns(null); - var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct); + var actual = await sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -149,9 +149,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, schema.Id, SearchScope.Published, A._)) .Returns(content); - var result = await sut.FindAsync(requestContext, schemaId.Name, DomainId.Create("_schemaId_"), ct: ct); + var actual = await sut.FindAsync(requestContext, schemaId.Name, DomainId.Create("_schemaId_"), ct: ct); - AssertContent(content, result); + AssertContent(content, actual); } [Theory] @@ -168,9 +168,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, scope, A._)) .Returns(content); - var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct); + var actual = await sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct); - AssertContent(content, result); + AssertContent(content, actual); } [Fact] @@ -183,9 +183,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, content.Id, 13, A._)) .Returns(content); - var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id, 13, ct); + var actual = await sut.FindAsync(requestContext, schemaId.Name, content.Id, 13, ct); - AssertContent(content, result); + AssertContent(content, actual); } [Fact] @@ -213,12 +213,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, q, scope, A._)) .Returns(ResultList.CreateFrom(5, content1, content2)); - var result = await sut.QueryAsync(requestContext, schemaId.Name, q, ct); + var actual = await sut.QueryAsync(requestContext, schemaId.Name, q, ct); - Assert.Equal(5, result.Total); + Assert.Equal(5, actual.Total); - AssertContent(content1, result[0]); - AssertContent(content2, result[1]); + AssertContent(content1, actual[0]); + AssertContent(content2, actual[1]); } [Theory] @@ -241,13 +241,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A._)) .Returns(ResultList.Create(5, contents)); - var result = await sut.QueryAsync(requestContext, q, ct); + var actual = await sut.QueryAsync(requestContext, q, ct); - Assert.Equal(5, result.Total); + Assert.Equal(5, actual.Total); for (var i = 0; i < contents.Count; i++) { - AssertContent(contents[i], result[i]); + AssertContent(contents[i], actual[i]); } } @@ -265,9 +265,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A._)) .Returns(ResultList.Create(0, ids.Select(CreateContent))); - var result = await sut.QueryAsync(requestContext, q, ct); + var actual = await sut.QueryAsync(requestContext, q, ct); - Assert.Empty(result); + Assert.Empty(actual); } [Fact] @@ -278,7 +278,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, ct); A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, - A.That.Matches(x => Equals(x.CreatedBy, requestContext.User.Token())), SearchScope.Published, A + A.That.Matches(x => Equals(x.CreatedBy, requestContext.UserPrincipal.Token())), SearchScope.Published, A ._)) .MustHaveHappened(); } @@ -333,12 +333,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return new Context(claimsPrincipal, Mocks.App(appId)).Clone(b => b.WithUnpublished(isUnpublished == 1)); } - private static void AssertContent(IContentEntity source, IEnrichedContentEntity? result) + private static void AssertContent(IContentEntity source, IEnrichedContentEntity? actual) { - Assert.NotNull(result); - Assert.NotSame(source, result); - Assert.Same(source.Data, result?.Data); - Assert.Equal(source.Id, result?.Id); + Assert.NotNull(actual); + Assert.NotSame(source, actual); + Assert.Same(source.Data, actual?.Data); + Assert.Equal(source.Id, actual?.Id); } private IContentEntity CreateContent(DomainId id) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs index 87dfbd67c..1d91ed6c9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries new StatusInfo(Status.Published, StatusColors.Published) }; - A.CallTo(() => workflow.GetNextAsync(content, content.Status, requestContext.User)) + A.CallTo(() => workflow.GetNextAsync(content, content.Status, requestContext.UserPrincipal)) .Returns(nexts); await sut.EnrichAsync(requestContext, new[] { content }, null!, default); @@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.Equal(Status.Published, content.NextStatuses?.Single().Status); - A.CallTo(() => workflow.GetNextAsync(content, A._, requestContext.User)) + A.CallTo(() => workflow.GetNextAsync(content, A._, requestContext.UserPrincipal)) .MustNotHaveHappened(); } @@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.Empty(content.NextStatuses); - A.CallTo(() => workflow.GetNextAsync(content, A._, requestContext.User)) + A.CallTo(() => workflow.GetNextAsync(content, A._, requestContext.UserPrincipal)) .MustNotHaveHappened(); } @@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var content = new ContentEntity { SchemaId = schemaId }; - A.CallTo(() => workflow.CanUpdateAsync(content, content.Status, requestContext.User)) + A.CallTo(() => workflow.CanUpdateAsync(content, content.Status, requestContext.UserPrincipal)) .Returns(true); var ctx = requestContext.Clone(b => b.WithResolveFlow(false)); @@ -156,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.False(content.CanUpdate); - A.CallTo(() => workflow.CanUpdateAsync(content, A._, requestContext.User)) + A.CallTo(() => workflow.CanUpdateAsync(content, A._, requestContext.UserPrincipal)) .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs index 82a48a5af..0efdbb7ba 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => scriptEngine.TransformAsync( A.That.Matches(x => - Equals(x["user"], ctx.User) && + Equals(x["user"], ctx.UserPrincipal) && Equals(x["data"], oldData) && Equals(x["contentId"], content.Id)), "my-query", diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs index 7b0614650..4ae113e2b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs @@ -86,9 +86,9 @@ namespace Squidex.Domain.Apps.Entities.Contents Text: Hello 2 World 2 {referenceId2} "; - var result = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -132,9 +132,9 @@ namespace Squidex.Domain.Apps.Entities.Contents Text: Hello 2 World 2 {referenceId2} "; - var result = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs index 79ff535fc..bf9d22c8f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs @@ -63,14 +63,14 @@ namespace Squidex.Domain.Apps.Entities.Contents var script = @" getReference(data.references.iv[0], function (references) { - var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; + var actual1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; - complete(`${result1}`); + complete(`${actual1}`); })"; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } [Fact] @@ -85,15 +85,15 @@ namespace Squidex.Domain.Apps.Entities.Contents var script = @" getReferences(data.references.iv, function (references) { - var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; - var result2 = `Text: ${references[1].data.field1.iv} ${references[1].data.field2.iv}`; + var actual1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; + var actual2 = `Text: ${references[1].data.field1.iv} ${references[1].data.field2.iv}`; - complete(`${result1}\n${result2}`); + complete(`${actual1}\n${actual2}`); })"; - var result = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(result)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); } private (ScriptVars, IContentEntity[]) SetupReferenceVars(int count) @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Contents .AddInvariant(JsonValue.Array(referenceIds))); A.CallTo(() => contentQuery.QueryAsync( - A.That.Matches(x => x.App.Id == appId.Id && x.User == user), A.That.HasIds(referenceIds), A._)) + A.That.Matches(x => x.App.Id == appId.Id && x.UserPrincipal == user), A.That.HasIds(referenceIds), A._)) .Returns(ResultList.CreateFrom(2, references)); var vars = new ScriptVars diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTestsBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTestsBase.cs index 755cd71a9..3566d5054 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTestsBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTestsBase.cs @@ -388,15 +388,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { var query = new GeoQuery(schemaId.Id, field, latitude, longitude, 1000, 1000); - var result = await Sut.TextIndex.SearchAsync(app, query, target); + var actual = await Sut.TextIndex.SearchAsync(app, query, target); if (expected != null) { - result.Should().BeEquivalentTo(expected.ToHashSet()); + actual.Should().BeEquivalentTo(expected.ToHashSet()); } else { - result.Should().BeEmpty(); + actual.Should().BeEmpty(); } } @@ -407,15 +407,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text RequiredSchemaIds = new List { schemaId.Id } }; - var result = await Sut.TextIndex.SearchAsync(app, query, target); + var actual = await Sut.TextIndex.SearchAsync(app, query, target); if (expected != null) { - result.Should().BeEquivalentTo(expected.ToHashSet()); + actual.Should().BeEquivalentTo(expected.ToHashSet()); } else { - result.Should().BeEmpty(); + actual.Should().BeEmpty(); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs new file mode 100644 index 000000000..cd9d7115e --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs @@ -0,0 +1,334 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Microsoft.Extensions.Logging; +using NodaTime; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.Notifications; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Teams; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Invitation +{ + public class InvitationEventConsumerTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly INotificationSender notificatíonSender = A.Fake(); + private readonly IUserResolver userResolver = A.Fake(); + private readonly IUser assigner = UserMocks.User("1"); + private readonly IUser assignee = UserMocks.User("2"); + private readonly ILogger log = A.Fake>(); + private readonly string assignerId = DomainId.NewGuid().ToString(); + private readonly string assigneeId = DomainId.NewGuid().ToString(); + private readonly string appName = "my-app"; + private readonly string teamName = "my-team"; + private readonly InvitationEventConsumer sut; + + public InvitationEventConsumerTests() + { + A.CallTo(() => notificatíonSender.IsActive) + .Returns(true); + + A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) + .Returns(assigner); + + A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) + .Returns(assignee); + + A.CallTo(() => appProvider.GetTeamAsync(A._, default)) + .Returns(Mocks.Team(DomainId.NewGuid(), teamName)); + + sut = new InvitationEventConsumer(notificatíonSender, userResolver, appProvider, log); + } + + [Fact] + public async Task Should_not_send_app_email_if_contributors_assigned_by_clients() + { + var @event = CreateAppEvent(RefTokenType.Client, true); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_app_email_for_initial_owner() + { + var @event = CreateAppEvent(RefTokenType.Subject, false, streamNumber: 1); + + await sut.On(@event); + + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_team_email_for_initial_owner() + { + var @event = CreateTeamEvent(false, streamNumber: 1); + + await sut.On(@event); + + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_app_email_for_old_events() + { + var created = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50)); + + var @event = CreateAppEvent(RefTokenType.Subject, true, instant: created); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_team_email_for_old_events() + { + var created = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50)); + + var @event = CreateTeamEvent(true, instant: created); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_app_email_for_old_contributor() + { + var @event = CreateAppEvent(RefTokenType.Subject, true, isNewContributor: false); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_team_email_for_old_contributor() + { + var @event = CreateTeamEvent(true, isNewContributor: false); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_app_email_if_sender_not_active() + { + var @event = CreateAppEvent(RefTokenType.Subject, true); + + A.CallTo(() => notificatíonSender.IsActive) + .Returns(false); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_team_email_if_sender_not_active() + { + var @event = CreateTeamEvent(true); + + A.CallTo(() => notificatíonSender.IsActive) + .Returns(false); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_app_email_if_assigner_not_found() + { + var @event = CreateAppEvent(RefTokenType.Subject, true); + + A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + MustNotSendEmail(); + MustLogWarning(); + } + + [Fact] + public async Task Should_not_send_team_email_if_assigner_not_found() + { + var @event = CreateTeamEvent(true); + + A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + MustNotSendEmail(); + MustLogWarning(); + } + + [Fact] + public async Task Should_not_send_app_email_if_assignee_not_found() + { + var @event = CreateAppEvent(RefTokenType.Subject, true); + + A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + MustNotSendEmail(); + MustLogWarning(); + } + + [Fact] + public async Task Should_not_send_team_email_if_assignee_not_found() + { + var @event = CreateTeamEvent(true); + + A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + MustNotSendEmail(); + MustLogWarning(); + } + + [Fact] + public async Task Should_send_app_email_for_new_user() + { + var @event = CreateAppEvent(RefTokenType.Subject, true); + + await sut.On(@event); + + A.CallTo(() => notificatíonSender.SendInviteAsync(assigner, assignee, appName)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_send_team_email_for_new_user() + { + var @event = CreateTeamEvent(true); + + await sut.On(@event); + + A.CallTo(() => notificatíonSender.SendTeamInviteAsync(assigner, assignee, teamName)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_send_app_email_for_existing_user() + { + var @event = CreateAppEvent(RefTokenType.Subject, false); + + await sut.On(@event); + + A.CallTo(() => notificatíonSender.SendInviteAsync(assigner, assignee, appName)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_send_team_email_for_existing_user() + { + var @event = CreateTeamEvent(false); + + await sut.On(@event); + + A.CallTo(() => notificatíonSender.SendTeamInviteAsync(assigner, assignee, teamName)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_send_team_email_if_team_not_found() + { + var @event = CreateTeamEvent(true); + + A.CallTo(() => appProvider.GetTeamAsync(A._, default)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + MustNotSendEmail(); + } + + private void MustLogWarning() + { + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument(0) == LogLevel.Warning) + .MustHaveHappened(); + } + + private void MustNotResolveUser() + { + A.CallTo(() => userResolver.FindByIdAsync(A._, A._)) + .MustNotHaveHappened(); + } + + private void MustNotSendEmail() + { + A.CallTo(() => notificatíonSender.SendInviteAsync(A._, A._, A._)) + .MustNotHaveHappened(); + + A.CallTo(() => notificatíonSender.SendTeamInviteAsync(A._, A._, A._)) + .MustNotHaveHappened(); + } + + private Envelope CreateAppEvent(RefTokenType assignerType, bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2) + { + var @event = new AppContributorAssigned + { + Actor = new RefToken(assignerType, assignerId), + AppId = NamedId.Of(DomainId.NewGuid(), appName), + ContributorId = assigneeId, + IsCreated = isNewUser, + IsAdded = isNewContributor + }; + + var envelope = Envelope.Create(@event); + + envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant()); + envelope.SetEventStreamNumber(streamNumber); + + return envelope; + } + + private Envelope CreateTeamEvent(bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2) + { + var @event = new TeamContributorAssigned + { + Actor = new RefToken(RefTokenType.Subject, assignerId), + ContributorId = assigneeId, + IsCreated = isNewUser, + IsAdded = isNewContributor, + TeamId = DomainId.NewGuid() + }; + + var envelope = Envelope.Create(@event); + + envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant()); + envelope.SetEventStreamNumber(streamNumber); + + return envelope; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InviteUserCommandMiddlewareTests.cs similarity index 55% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InviteUserCommandMiddlewareTests.cs index e42e6958b..f0596599b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InviteUserCommandMiddlewareTests.cs @@ -7,14 +7,18 @@ using FakeItEasy; using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Teams; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Shared.Users; using Xunit; +using AssignAppContributor = Squidex.Domain.Apps.Entities.Apps.Commands.AssignContributor; +using AssignTeamContributor = Squidex.Domain.Apps.Entities.Teams.Commands.AssignContributor; -namespace Squidex.Domain.Apps.Entities.Apps.Invitation +namespace Squidex.Domain.Apps.Entities.Invitation { public class InviteUserCommandMiddlewareTests { @@ -22,6 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation private readonly CancellationToken ct; private readonly IUserResolver userResolver = A.Fake(); private readonly IAppEntity app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + private readonly ITeamEntity team = Mocks.Team(DomainId.NewGuid()); private readonly ICommandBus commandBus = A.Fake(); private readonly InviteUserCommandMiddleware sut; @@ -33,9 +38,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation } [Fact] - public async Task Should_invite_user_and_change_result_and_update_command() + public async Task Should_invite_user_to_app_and_update_command() { - var command = new AssignContributor { ContributorId = "me@email.com", Invite = true }; + var command = new AssignAppContributor { ContributorId = "me@email.com", Invite = true }; var context = new CommandContext(command, commandBus) @@ -48,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation await sut.HandleAsync(context, ct); - Assert.Same(context.Result().App, app); + Assert.Same(context.Result>().Entity, app); Assert.Equal(user.Id, command.ContributorId); A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) @@ -56,9 +61,32 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation } [Fact] - public async Task Should_invite_user_and_not_change_result_if_not_added() + public async Task Should_invite_user_to_team_and_update_command() { - var command = new AssignContributor { ContributorId = "me@email.com", Invite = true }; + var command = new AssignTeamContributor { ContributorId = "me@email.com", Invite = true }; + + var context = + new CommandContext(command, commandBus) + .Complete(team); + + var user = UserMocks.User("123", command.ContributorId); + + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .Returns((user, true)); + + await sut.HandleAsync(context, ct); + + Assert.Same(context.Result>().Entity, team); + Assert.Equal(user.Id, command.ContributorId); + + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_invite_user_to_app_but_do_not_change_command_if_not_created() + { + var command = new AssignAppContributor { ContributorId = "me@email.com", Invite = true }; var context = new CommandContext(command, commandBus) @@ -78,10 +106,33 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation .MustHaveHappened(); } + [Fact] + public async Task Should_invite_user_to_team_but_do_not_change_command_if_not_created() + { + var command = new AssignTeamContributor { ContributorId = "me@email.com", Invite = true }; + + var context = + new CommandContext(command, commandBus) + .Complete(team); + + var user = UserMocks.User("123", command.ContributorId); + + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .Returns((user, false)); + + await sut.HandleAsync(context, ct); + + Assert.Same(context.Result(), team); + Assert.Equal(user.Id, command.ContributorId); + + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .MustHaveHappened(); + } + [Fact] public async Task Should_not_call_user_resolver_if_not_email() { - var command = new AssignContributor { ContributorId = "123", Invite = true }; + var command = new AssignAppContributor { ContributorId = "123", Invite = true }; var context = new CommandContext(command, commandBus) @@ -96,7 +147,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation [Fact] public async Task Should_not_call_user_resolver_if_not_inviting() { - var command = new AssignContributor { ContributorId = "123", Invite = false }; + var command = new AssignAppContributor { ContributorId = "123", Invite = false }; var context = new CommandContext(command, commandBus) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/NotificationEmailSenderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/NotificationEmailSenderTests.cs index 96d2696f3..e9884fbb7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/NotificationEmailSenderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/NotificationEmailSenderTests.cs @@ -145,25 +145,25 @@ namespace Squidex.Domain.Apps.Entities.Notifications .MustHaveHappened(); } - private async Task TestUsageFormattingAsync(string pattern, string result) + private async Task TestUsageFormattingAsync(string pattern, string actual) { texts.UsageSubject = pattern; texts.UsageBody = pattern; await sut.SendUsageAsync(assigned, appName, 100, 120); - A.CallTo(() => emailSender.SendAsync(assigned.Email, result, result, A._)) + A.CallTo(() => emailSender.SendAsync(assigned.Email, actual, actual, A._)) .MustHaveHappened(); } - private async Task TestInvitationFormattingAsync(string pattern, string result) + private async Task TestInvitationFormattingAsync(string pattern, string actual) { texts.NewUserSubject = pattern; texts.NewUserBody = pattern; await sut.SendInviteAsync(assigner, assigned, appName); - A.CallTo(() => emailSender.SendAsync(assigned.Email, result, result, A._)) + A.CallTo(() => emailSender.SendAsync(assigned.Email, actual, actual, A._)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs index 0092da81a..4d8c06e0f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs @@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Rules { @event.AppId = appId; - return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId.Id, @event.RuleId)); + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.RuleId)); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs index fca723207..60d0f95c8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject } [Fact] - public async Task Should_not_invoke_enricher_for_other_result() + public async Task Should_not_invoke_enricher_for_other_actual() { await HandleAsync(new EnableRule(), 12); @@ -54,36 +54,36 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject [Fact] public async Task Should_not_invoke_enricher_if_already_enriched() { - var result = new RuleEntity(); + var actual = new RuleEntity(); var context = await HandleAsync(new EnableRule(), - result); + actual); - Assert.Same(result, context.Result()); + Assert.Same(actual, context.Result()); A.CallTo(() => ruleEnricher.EnrichAsync(A._, requestContext, default)) .MustNotHaveHappened(); } [Fact] - public async Task Should_enrich_rule_result() + public async Task Should_enrich_rule_actual() { - var result = A.Fake(); + var actual = A.Fake(); var enriched = new RuleEntity(); - A.CallTo(() => ruleEnricher.EnrichAsync(result, requestContext, default)) + A.CallTo(() => ruleEnricher.EnrichAsync(actual, requestContext, default)) .Returns(enriched); var context = await HandleAsync(new EnableRule(), - result); + actual); Assert.Same(enriched, context.Result()); } - private Task HandleAsync(RuleCommand command, object result) + private Task HandleAsync(RuleCommand command, object actual) { command.RuleId = ruleId; @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject var domainObject = A.Fake(); A.CallTo(() => domainObject.ExecuteAsync(A._, A._)) - .Returns(new CommandResult(command.AggregateId, 1, 0, result)); + .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); A.CallTo(() => domainObjectFactory.Create(command.AggregateId)) .Returns(domainObject); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs index 265938765..1ed66f905 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs @@ -65,9 +65,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject { var command = MakeCreateCommand(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(AppId, sut.Snapshot.AppId.Id); @@ -87,9 +87,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.RuleDef.IsEnabled); @@ -112,9 +112,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject await ExecuteCreateAsync(); await ExecuteDisableAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.RuleDef.IsEnabled); @@ -135,9 +135,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject await ExecuteCreateAsync(); await ExecuteDisableAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.RuleDef.IsEnabled); @@ -154,9 +154,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.RuleDef.IsEnabled); @@ -176,9 +176,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.RuleDef.IsEnabled); @@ -195,9 +195,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); Assert.True(sut.Snapshot.IsDeleted); @@ -238,20 +238,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject return PublishAsync(new DeleteRule()); } - private T CreateRuleEvent(T @event) where T : RuleEvent - { - @event.RuleId = ruleId; - - return CreateEvent(@event); - } - - private T CreateRuleCommand(T command) where T : RuleCommand - { - command.RuleId = ruleId; - - return CreateCommand(command); - } - private static CreateRule MakeCreateCommand() { return new CreateRule @@ -283,6 +269,20 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject }; } + private T CreateRuleEvent(T @event) where T : RuleEvent + { + @event.RuleId = ruleId; + + return CreateEvent(@event); + } + + private T CreateRuleCommand(T command) where T : RuleCommand + { + command.RuleId = ruleId; + + return CreateCommand(command); + } + private Task PublishIdempotentAsync(RuleCommand command) { return PublishIdempotentAsync(sut, CreateRuleCommand(command)); @@ -290,9 +290,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject private async Task PublishAsync(RuleCommand command) { - var result = await sut.ExecuteAsync(CreateRuleCommand(command), default); + var actual = await sut.ExecuteAsync(CreateRuleCommand(command), default); - return result.Payload; + return actual.Payload; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs index 50f365cfb..174fb978e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using FakeItEasy; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.TestHelpers; @@ -41,9 +40,9 @@ namespace Squidex.Domain.Apps.Entities.Rules var @event = TestUtils.CreateEvent(); var envelope = Envelope.Create(@event); - var result = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); - var enrichedEvent = (EnrichedManualEvent)result.Single(); + var enrichedEvent = (EnrichedManualEvent)actual.Single(); Assert.Equal(@event.Actor, enrichedEvent.Actor); Assert.Equal(@event.AppId, enrichedEvent.AppId); @@ -58,9 +57,9 @@ namespace Squidex.Domain.Apps.Entities.Rules var @event = new RuleManuallyTriggered { Actor = actor }; var envelope = Envelope.Create(@event); - var result = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); - Assert.Equal(actor, ((EnrichedUserEventBase)result.Single()).Actor); + Assert.Equal(actor, ((EnrichedUserEventBase)actual.Single()).Actor); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs index 71c4e8243..8362144e7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs @@ -39,12 +39,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries { var source = CreateRule(); - var result = await sut.EnrichAsync(source, requestContext, ct); + var actual = await sut.EnrichAsync(source, requestContext, ct); - Assert.Equal(0, result.NumFailed); - Assert.Equal(0, result.NumSucceeded); + Assert.Equal(0, actual.NumFailed); + Assert.Equal(0, actual.NumSucceeded); - Assert.Null(result.LastExecuted); + Assert.Null(actual.LastExecuted); A.CallTo(() => requestCache.AddDependency(source.UniqueId, source.Version)) .MustHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs index 4dec74d2a..b72bb0aff 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs @@ -51,9 +51,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries A.CallTo(() => ruleEnricher.EnrichAsync(original, requestContext, ct)) .Returns(enriched); - var result = await sut.QueryAsync(requestContext, ct); + var actual = await sut.QueryAsync(requestContext, ct); - Assert.Same(enriched, result); + Assert.Same(enriched, actual); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerWorkerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerWorkerTests.cs index d804dc336..297a0d958 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerWorkerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerWorkerTests.cs @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Rules [InlineData(2, 360, RuleResult.Failed, RuleJobResult.Retry)] [InlineData(3, 720, RuleResult.Failed, RuleJobResult.Retry)] [InlineData(4, 0, RuleResult.Failed, RuleJobResult.Failed)] - public async Task Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult result, RuleJobResult jobResult) + public async Task Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult actual, RuleJobResult jobResult) { var actionData = "{}"; var actionName = "MyAction"; @@ -112,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var requestDump = "Dump"; A.CallTo(() => ruleService.InvokeAsync(@event.Job.ActionName, @event.Job.ActionData, default)) - .Returns((Result.Create(requestDump, result), requestElapsed)); + .Returns((Result.Create(requestDump, actual), requestElapsed)); var now = clock.GetCurrentInstant(); @@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Rules await sut.HandleAsync(@event); - if (result == RuleResult.Failed) + if (actual == RuleResult.Failed) { A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument(0) == LogLevel.Warning) .MustHaveHappened(); @@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Rules A.That.Matches(x => x.Elapsed == requestElapsed && x.ExecutionDump == requestDump && - x.ExecutionResult == result && + x.ExecutionResult == actual && x.Finished == now && x.JobNext == nextCall && x.JobResult == jobResult), diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs index d49597529..c1efc2f83 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs @@ -48,9 +48,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 }; var envelope = Envelope.Create(@event); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - var enrichedEvent = result.Single() as EnrichedUsageExceededEvent; + var enrichedEvent = actual.Single() as EnrichedUsageExceededEvent; Assert.Equal(@event.CallsCurrent, enrichedEvent!.CallsCurrent); Assert.Equal(@event.CallsLimit, enrichedEvent!.CallsLimit); @@ -63,9 +63,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking var @event = new AppUsageExceeded(); - var result = sut.Trigger(Envelope.Create(@event), ctx); + var actual = sut.Trigger(Envelope.Create(@event), ctx); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -75,9 +75,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking var @event = new AppUsageExceeded { RuleId = ctx.RuleId }; - var result = sut.Trigger(Envelope.Create(@event), ctx); + var actual = sut.Trigger(Envelope.Create(@event), ctx); - Assert.True(result); + Assert.True(actual); } private static RuleContext Context(RuleTrigger? trigger = null) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs index 8fb5b4861..f3e79498d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs @@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas { @event.AppId = appId; - return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId.Id, @event.SchemaId.Id)); + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.SchemaId.Id)); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs index 7194499c9..e53353346 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs @@ -58,9 +58,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties, Type = SchemaType.Singleton }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(AppId, sut.Snapshot.AppId.Id); @@ -97,9 +97,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties, Fields = fields }; - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); var @event = (SchemaCreated)LastEvents.Single().Payload; @@ -117,9 +117,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.Properties); @@ -142,9 +142,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal("", sut.Snapshot.SchemaDef.Scripts.Query); @@ -167,9 +167,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.NotEmpty(sut.Snapshot.SchemaDef.FieldRules); @@ -190,9 +190,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.FieldsInLists, sut.Snapshot.SchemaDef.FieldsInLists); @@ -213,9 +213,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.FieldsInReferences, sut.Snapshot.SchemaDef.FieldsInReferences); @@ -232,9 +232,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.SchemaDef.IsPublished); @@ -252,9 +252,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); await ExecutePublishAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.SchemaDef.IsPublished); @@ -271,9 +271,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Name, sut.Snapshot.SchemaDef.Category); @@ -296,9 +296,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.PreviewUrls, sut.Snapshot.SchemaDef.PreviewUrls); @@ -315,9 +315,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); Assert.True(sut.Snapshot.IsDeleted); @@ -336,9 +336,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteAddFieldAsync("field1"); await ExecuteAddFieldAsync("field2"); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -356,9 +356,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteAddFieldAsync("field1", 1); await ExecuteAddFieldAsync("field2", 1); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -373,9 +373,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Properties, GetField(1).RawProperties); @@ -393,9 +393,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); await ExecuteAddArrayFieldAsync(); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); @@ -413,9 +413,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Properties, GetField(1).RawProperties); @@ -434,9 +434,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteAddArrayFieldAsync(); await ExecuteAddFieldAsync(fieldName, 1); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); @@ -454,9 +454,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(GetField(1).IsLocked); @@ -475,9 +475,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteAddArrayFieldAsync(); await ExecuteAddFieldAsync(fieldName, 1); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(GetNestedField(1, 2).IsLocked); @@ -495,9 +495,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(GetField(1).IsHidden); @@ -516,9 +516,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteAddArrayFieldAsync(); await ExecuteAddFieldAsync(fieldName, 1); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(GetNestedField(1, 2).IsHidden); @@ -537,9 +537,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteAddFieldAsync(fieldName); await ExecuteHideFieldAsync(1); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.False(GetField(1).IsHidden); @@ -559,9 +559,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteAddFieldAsync(fieldName, 1); await ExecuteHideFieldAsync(2, 1); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.False(GetNestedField(1, 2).IsHidden); @@ -579,9 +579,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(GetField(1).IsDisabled); @@ -600,9 +600,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteAddArrayFieldAsync(); await ExecuteAddFieldAsync(fieldName, 1); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.True(GetNestedField(1, 2).IsDisabled); @@ -621,9 +621,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteAddFieldAsync(fieldName); await ExecuteDisableFieldAsync(1); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.False(GetField(1).IsDisabled); @@ -643,9 +643,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteAddFieldAsync(fieldName, 1); await ExecuteDisableFieldAsync(2, 1); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.False(GetNestedField(1, 2).IsDisabled); @@ -663,9 +663,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); await ExecuteAddFieldAsync(fieldName); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Null(GetField(1)); @@ -684,9 +684,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteAddArrayFieldAsync(); await ExecuteAddFieldAsync(fieldName, 1); - var result = await PublishAsync(command); + var actual = await PublishAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Null(GetNestedField(1, 2)); @@ -706,9 +706,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject await ExecuteCreateAsync(); - var result = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Category, sut.Snapshot.SchemaDef.Category); @@ -770,7 +770,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject private async Task PublishIdempotentAsync(T command) where T : SquidexCommand, IAggregateCommand { - var result = await PublishAsync(command); + var actual = await PublishAsync(command); var previousSnapshot = sut.Snapshot; var previousVersion = sut.Snapshot.Version; @@ -780,14 +780,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject Assert.Same(previousSnapshot, sut.Snapshot); Assert.Equal(previousVersion, sut.Snapshot.Version); - return result; + return actual; } private async Task PublishAsync(T command) where T : SquidexCommand, IAggregateCommand { - var result = await sut.ExecuteAsync(CreateCommand(command), default); + var actual = await sut.ExecuteAsync(CreateCommand(command), default); - return result.Payload; + return actual.Payload; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs index 4966784c8..cae4c6ebc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs @@ -12,7 +12,6 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.TestHelpers; -using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Schemas; @@ -73,9 +72,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas var envelope = Envelope.Create(@event).SetEventStreamNumber(12); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - var enrichedEvent = result.Single() as EnrichedSchemaEvent; + var enrichedEvent = actual.Single() as EnrichedSchemaEvent; Assert.Equal(type, enrichedEvent!.Type); Assert.Equal(@event.Actor, enrichedEvent.Actor); @@ -92,9 +91,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas { var @event = new SchemaCreated(); - var result = sut.Trigger(Envelope.Create(@event), ctx); + var actual = sut.Trigger(Envelope.Create(@event), ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -105,9 +104,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas { var @event = new EnrichedSchemaEvent(); - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -118,9 +117,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas { var @event = new EnrichedSchemaEvent(); - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(result); + Assert.True(actual); }); } @@ -131,9 +130,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas { var @event = new EnrichedSchemaEvent(); - var result = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.False(result); + Assert.False(actual); }); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs index a6a051e1e..a27248ac9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas } [Fact] - public async Task Should_not_add_result_to_contents_if_user_has_no_permission() + public async Task Should_not_add_actual_to_contents_if_user_has_no_permission() { var ctx = ContextWithPermission(); @@ -45,15 +45,15 @@ namespace Squidex.Domain.Apps.Entities.Schemas A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) .Returns("schemaA1-url"); - var result = await sut.SearchAsync("schema", ctx, default); + var actual = await sut.SearchAsync("schema", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url")); } [Fact] - public async Task Should_not_add_result_to_contents_if_schema_is_component() + public async Task Should_not_add_actual_to_contents_if_schema_is_component() { var permission = PermissionIds.ForApp(PermissionIds.AppContentsReadOwn, appId.Name, "schemaA1"); @@ -67,15 +67,15 @@ namespace Squidex.Domain.Apps.Entities.Schemas A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) .Returns("schemaA1-url"); - var result = await sut.SearchAsync("schema", ctx, default); + var actual = await sut.SearchAsync("schema", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url")); } [Fact] - public async Task Should_return_result_to_schema_and_contents_if_matching_and_permission_given() + public async Task Should_return_actual_to_schema_and_contents_if_matching_and_permission_given() { var permission = PermissionIds.ForApp(PermissionIds.AppContentsReadOwn, appId.Name, "schemaA2"); @@ -97,9 +97,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas A.CallTo(() => urlGenerator.ContentsUI(appId, schema2.NamedId())) .Returns("schemaA2-contents-url"); - var result = await sut.SearchAsync("schemaA", ctx, default); + var actual = await sut.SearchAsync("schemaA", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url") .Add("schemaA2 Schema", SearchResultType.Schema, "schemaA2-url") @@ -107,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas } [Fact] - public async Task Should_return_result_to_schema_and_contents_if_schema_is_singleton() + public async Task Should_return_actual_to_schema_and_contents_if_schema_is_singleton() { var permission = PermissionIds.ForApp(PermissionIds.AppContentsReadOwn, appId.Name, "schemaA1"); @@ -124,9 +124,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas A.CallTo(() => urlGenerator.ContentUI(appId, schema1.NamedId(), schema1.Id)) .Returns("schemaA1-content-url"); - var result = await sut.SearchAsync("schemaA", ctx, default); + var actual = await sut.SearchAsync("schemaA", ctx, default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url") .Add("schemaA1 Content", SearchResultType.Content, "schemaA1-content-url", "schemaA1")); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs index ba585f4c4..9cf0f355f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs @@ -30,9 +30,9 @@ namespace Squidex.Domain.Apps.Entities.Search [Fact] public async Task Should_not_call_sources_and_return_empty_if_query_is_empty() { - var result = await sut.SearchAsync(string.Empty, requestContext); + var actual = await sut.SearchAsync(string.Empty, requestContext); - Assert.Empty(result); + Assert.Empty(actual); A.CallTo(() => source1.SearchAsync(A._, A._, A._)) .MustNotHaveHappened(); @@ -44,9 +44,9 @@ namespace Squidex.Domain.Apps.Entities.Search [Fact] public async Task Should_not_call_sources_and_return_empty_if_is_too_short() { - var result = await sut.SearchAsync("11", requestContext); + var actual = await sut.SearchAsync("11", requestContext); - Assert.Empty(result); + Assert.Empty(actual); A.CallTo(() => source1.SearchAsync(A._, A._, A._)) .MustNotHaveHappened(); @@ -56,22 +56,22 @@ namespace Squidex.Domain.Apps.Entities.Search } [Fact] - public async Task Should_aggregate_results_from_all_sources() + public async Task Should_aggregate_actuals_from_all_sources() { - var result1 = new SearchResults().Add("Name1", SearchResultType.Setting, "Url1"); - var result2 = new SearchResults().Add("Name2", SearchResultType.Setting, "Url2"); + var actual1 = new SearchResults().Add("Name1", SearchResultType.Setting, "Url1"); + var actual2 = new SearchResults().Add("Name2", SearchResultType.Setting, "Url2"); var query = "a query"; A.CallTo(() => source1.SearchAsync(query, requestContext, A._)) - .Returns(result1); + .Returns(actual1); A.CallTo(() => source2.SearchAsync(query, requestContext, A._)) - .Returns(result2); + .Returns(actual2); - var result = await sut.SearchAsync(query, requestContext); + var actual = await sut.SearchAsync(query, requestContext); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new SearchResults() .Add("Name1", SearchResultType.Setting, "Url1") .Add("Name2", SearchResultType.Setting, "Url2")); @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Search [Fact] public async Task Should_ignore_exception_from_source() { - var result2 = new SearchResults().Add("Name2", SearchResultType.Setting, "Url2"); + var actual2 = new SearchResults().Add("Name2", SearchResultType.Setting, "Url2"); var query = "a query"; @@ -88,11 +88,11 @@ namespace Squidex.Domain.Apps.Entities.Search .Throws(new InvalidOperationException()); A.CallTo(() => source2.SearchAsync(query, requestContext, A._)) - .Returns(result2); + .Returns(actual2); - var result = await sut.SearchAsync(query, requestContext); + var actual = await sut.SearchAsync(query, requestContext); - result.Should().BeEquivalentTo(result2); + actual.Should().BeEquivalentTo(actual2); A.CallTo(log).Where(x => x.Method.Name == "Log") .MustHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index a2c454bdd..460bf30ce 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -78,4 +78,7 @@ PreserveNewest + + + \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamContributorsTests.cs new file mode 100644 index 000000000..4cd8b3940 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamContributorsTests.cs @@ -0,0 +1,198 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Squidex.Shared.Users; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject.Guards +{ + public class GuardTeamContributorsTests : IClassFixture + { + private readonly IUser user1 = UserMocks.User("1"); + private readonly IUser user2 = UserMocks.User("2"); + private readonly IUser user3 = UserMocks.User("3"); + private readonly IUserResolver users = A.Fake(); + private readonly Contributors contributors_0 = Contributors.Empty; + + public GuardTeamContributorsTests() + { + A.CallTo(() => user1.Id) + .Returns("1"); + + A.CallTo(() => user2.Id) + .Returns("2"); + + A.CallTo(() => user3.Id) + .Returns("3"); + + A.CallTo(() => users.FindByIdAsync("1", default)) + .Returns(user1); + + A.CallTo(() => users.FindByIdAsync("2", default)) + .Returns(user2); + + A.CallTo(() => users.FindByIdAsync("3", default)) + .Returns(user3); + + A.CallTo(() => users.FindByIdAsync("notfound", default)) + .Returns(Task.FromResult(null)); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_contributor_id_is_null() + { + var command = new AssignContributor(); + + await ValidationAssert.ThrowsAsync(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users), + new ValidationError("Contributor ID or email is required.", "ContributorId")); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_role_not_valid() + { + var command = new AssignContributor { ContributorId = "1", Role = "Invalid" }; + + await ValidationAssert.ThrowsAsync(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users), + new ValidationError("Role is not a valid value.", "Role")); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_same_role() + { + var command = new AssignContributor { ContributorId = "1", Role = Role.Owner }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + + await GuardTeamContributors.CanAssign(command, Team(contributors_1), users); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_some_role_but_is_from_restore() + { + var command = new AssignContributor { ContributorId = "1", Role = Role.Owner, IgnoreActor = true }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + + await GuardTeamContributors.CanAssign(command, Team(contributors_1), users); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_user_not_found() + { + var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner }; + + await Assert.ThrowsAsync(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users)); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_user_is_actor() + { + var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = RefToken.User("3") }; + + await Assert.ThrowsAsync(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users)); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_found() + { + var command = new AssignContributor { ContributorId = "1" }; + + await GuardTeamContributors.CanAssign(command, Team(contributors_0), users); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_role_is_valid() + { + var command = new AssignContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + + await GuardTeamContributors.CanAssign(command, Team(contributors_1), users); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_role_changed() + { + var command = new AssignContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Owner); + + await GuardTeamContributors.CanAssign(command, Team(contributors_2), users); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_ígnored() + { + var command = new AssignContributor { ContributorId = "3", IgnorePlans = true }; + + var contributors_1 = contributors_0.Assign("1", Role.Editor); + var contributors_2 = contributors_1.Assign("2", Role.Editor); + + await GuardTeamContributors.CanAssign(command, Team(contributors_2), users); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_id_is_null() + { + var command = new RemoveContributor(); + + ValidationAssert.Throws(() => GuardTeamContributors.CanRemove(command, Team(contributors_0)), + new ValidationError("Contributor ID or email is required.", "ContributorId")); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_not_found() + { + var command = new RemoveContributor { ContributorId = "1" }; + + Assert.Throws(() => GuardTeamContributors.CanRemove(command, Team(contributors_0))); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_is_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Editor); + + ValidationAssert.Throws(() => GuardTeamContributors.CanRemove(command, Team(contributors_2)), + new ValidationError("Cannot remove the only owner.")); + } + + [Fact] + public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Owner); + + GuardTeamContributors.CanRemove(command, Team(contributors_2)); + } + + private static ITeamEntity Team(Contributors contributors) + { + var team = A.Fake(); + + A.CallTo(() => team.Contributors).Returns(contributors); + + return team; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamTests.cs new file mode 100644 index 000000000..71cdd2a16 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamTests.cs @@ -0,0 +1,86 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.Billing; +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Domain.Apps.Entities.Teams.DomainObject.Guards; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Teams.Apps.Teams.DomainObject.Guards +{ + public class GuardTeamTests : IClassFixture + { + private readonly IUserResolver users = A.Fake(); + private readonly IBillingPlans billingPlans = A.Fake(); + private readonly Plan planBasic = new Plan(); + private readonly Plan planFree = new Plan(); + + public GuardTeamTests() + { + A.CallTo(() => users.FindByIdOrEmailAsync(A._, default)) + .Returns(A.Dummy()); + + A.CallTo(() => billingPlans.GetPlan("notfound")) + .Returns(null!); + + A.CallTo(() => billingPlans.GetPlan("basic")) + .Returns(planBasic); + + A.CallTo(() => billingPlans.GetPlan("free")) + .Returns(planFree); + } + + [Fact] + public void CanCreate_should_throw_exception_if_name_not_valid() + { + var command = new CreateTeam { Name = null! }; + + ValidationAssert.Throws(() => GuardTeam.CanCreate(command), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanCreate_should_not_throw_exception_if_team_name_is_valid() + { + var command = new CreateTeam { Name = "new-team" }; + + GuardTeam.CanCreate(command); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_id_is_null() + { + var command = new ChangePlan { Actor = RefToken.User("me") }; + + ValidationAssert.Throws(() => GuardTeam.CanChangePlan(command, billingPlans), + new ValidationError("Plan ID is required.", "PlanId")); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_not_found() + { + var command = new ChangePlan { PlanId = "notfound", Actor = RefToken.User("me") }; + + ValidationAssert.Throws(() => GuardTeam.CanChangePlan(command, billingPlans), + new ValidationError("A plan with this id does not exist.", "PlanId")); + } + + [Fact] + public void CanChangePlan_should_not_throw_exception_if_plan_is_found() + { + var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; + + GuardTeam.CanChangePlan(command, billingPlans); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/TeamDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/TeamDomainObjectTests.cs new file mode 100644 index 000000000..b2b1a3246 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/TeamDomainObjectTests.cs @@ -0,0 +1,350 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.Billing; +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Teams; +using Squidex.Infrastructure; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject +{ + public class TeamDomainObjectTests : HandlerTestBase + { + private readonly IBillingPlans billingPlans = A.Fake(); + private readonly IBillingManager billingManager = A.Fake(); + private readonly IUser user; + private readonly IUserResolver userResolver = A.Fake(); + private readonly Plan planPaid = new Plan { Id = "premium" }; + private readonly Plan planFree = new Plan { Id = "free" }; + private readonly string contributorId = DomainId.NewGuid().ToString(); + private readonly string name = "My Team"; + private readonly DomainId teamId = DomainId.NewGuid(); + private readonly TeamDomainObject sut; + + protected override DomainId Id + { + get => teamId; + } + + public TeamDomainObjectTests() + { + user = UserMocks.User(contributorId); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId, default)) + .Returns(user); + + A.CallTo(() => billingPlans.GetFreePlan()) + .Returns(planFree); + + A.CallTo(() => billingPlans.GetPlan(planFree.Id)) + .Returns(planFree); + + A.CallTo(() => billingPlans.GetPlan(planPaid.Id)) + .Returns(planPaid); + + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, teamId, A._, default)) + .Returns(Task.FromResult(null)); + + var serviceProvider = + new ServiceCollection() + .AddSingleton(billingPlans) + .AddSingleton(billingManager) + .AddSingleton(userResolver) + .BuildServiceProvider(); + + var log = A.Fake>(); + +#pragma warning disable MA0056 // Do not call overridable members in constructor + sut = new TeamDomainObject(Id, PersistenceFactory, log, serviceProvider); +#pragma warning restore MA0056 // Do not call overridable members in constructor + } + + [Fact] + public async Task Create_should_create_events_and_set_intitial_state() + { + var command = new CreateTeam { Name = name }; + + var actual = await PublishAsync(command); + + actual.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(name, sut.Snapshot.Name); + + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamCreated { Name = name }), + CreateTeamEvent(new TeamContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }) + ); + } + + [Fact] + public async Task Create_should_not_assign_client_as_contributor() + { + var command = new CreateTeam { Name = name, Actor = ActorClient }; + + var actual = await PublishAsync(command); + + actual.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(name, sut.Snapshot.Name); + + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamCreated { Name = name }, true) // Must be with client actor. + ); + } + + [Fact] + public async Task Update_should_create_events_and_update_label_and_description() + { + var command = new UpdateTeam { Name = "Changed Name" }; + + await ExecuteCreateAsync(); + + var actual = await PublishIdempotentAsync(command); + + actual.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(command.Name, sut.Snapshot.Name); + + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamUpdated { Name = command.Name }) + ); + } + + [Fact] + public async Task ChangePlan_should_create_events_and_update_plan() + { + var command = new ChangePlan { PlanId = planPaid.Id }; + + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, teamId, planPaid.Id, default)) + .Returns(Task.FromResult(null)); + + await ExecuteCreateAsync(); + + var actual = await PublishIdempotentAsync(command); + + actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id)); + + Assert.Equal(planPaid.Id, sut.Snapshot.Plan!.PlanId); + + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamPlanChanged { PlanId = planPaid.Id }) + ); + + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, teamId, planPaid.Id, default)) + .MustHaveHappened(); + + A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, teamId, planPaid.Id, default)) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangePlan_from_callback_should_create_events_and_update_plan() + { + var command = new ChangePlan { PlanId = planPaid.Id, FromCallback = true }; + + await ExecuteCreateAsync(); + + var actual = await PublishIdempotentAsync(command); + + actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id)); + + Assert.Equal(planPaid.Id, sut.Snapshot.Plan!.PlanId); + + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamPlanChanged { PlanId = planPaid.Id }) + ); + + A.CallTo(() => billingManager.MustRedirectToPortalAsync(A._, A._, A._, A._)) + .MustNotHaveHappened(); + + A.CallTo(() => billingManager.SubscribeAsync(A._, A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ChangePlan_from_callback_should_reset_plan_for_free_plan() + { + var command = new ChangePlan { PlanId = planFree.Id, FromCallback = true }; + + await ExecuteCreateAsync(); + await ExecuteChangePlanAsync(); + + var actual = await PublishIdempotentAsync(command); + + actual.ShouldBeEquivalent(new PlanChangedResult(planFree.Id, true)); + + Assert.Null(sut.Snapshot.Plan); + + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamPlanReset()) + ); + + A.CallTo(() => billingManager.MustRedirectToPortalAsync(A._, teamId, A._, A._)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => billingManager.UnsubscribeAsync(A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ChangePlan_should_reset_plan_for_free_plan() + { + var command = new ChangePlan { PlanId = planFree.Id }; + + await ExecuteCreateAsync(); + await ExecuteChangePlanAsync(); + + var actual = await PublishIdempotentAsync(command); + + actual.ShouldBeEquivalent(new PlanChangedResult(planFree.Id, true)); + + Assert.Null(sut.Snapshot.Plan); + + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamPlanReset()) + ); + + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, teamId, planPaid.Id, default)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => billingManager.UnsubscribeAsync(A._, teamId, A._)) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangePlan_should_not_make_update_for_redirect_actual() + { + var command = new ChangePlan { PlanId = planPaid.Id }; + + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, teamId, planPaid.Id, default)) + .Returns(new Uri("http://squidex.io")); + + await ExecuteCreateAsync(); + + var actual = await PublishIdempotentAsync(command); + + actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id, false, new Uri("http://squidex.io"))); + + Assert.Null(sut.Snapshot.Plan); + } + + [Fact] + public async Task ChangePlan_should_not_call_billing_manager_for_callback() + { + var command = new ChangePlan { PlanId = planPaid.Id, FromCallback = true }; + + await ExecuteCreateAsync(); + + var actual = await PublishIdempotentAsync(command); + + actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id)); + + Assert.Equal(planPaid.Id, sut.Snapshot.Plan?.PlanId); + + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, teamId, planPaid.Id, A._)) + .MustNotHaveHappened(); + + A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, teamId, planPaid.Id, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task AssignContributor_should_create_events_and_add_contributor() + { + var command = new AssignContributor { ContributorId = contributorId }; + + await ExecuteCreateAsync(); + + var actual = await PublishIdempotentAsync(command); + + actual.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(command.Role, sut.Snapshot.Contributors[contributorId]); + + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamContributorAssigned { ContributorId = contributorId, Role = command.Role, IsAdded = true }) + ); + } + + [Fact] + public async Task RemoveContributor_should_create_events_and_remove_contributor() + { + var command = new RemoveContributor { ContributorId = contributorId }; + + await ExecuteCreateAsync(); + await ExecuteAssignContributorAsync(); + + var actual = await PublishAsync(command); + + actual.ShouldBeEquivalent(sut.Snapshot); + + Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); + + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamContributorRemoved { ContributorId = contributorId }) + ); + } + + private Task ExecuteCreateAsync() + { + return PublishAsync(new CreateTeam { Name = name }); + } + + private Task ExecuteAssignContributorAsync() + { + return PublishAsync(new AssignContributor { ContributorId = contributorId }); + } + + private Task ExecuteChangePlanAsync() + { + return PublishAsync(new ChangePlan { PlanId = planPaid.Id }); + } + + private T CreateTeamEvent(T @event, bool fromClient = false) where T : TeamEvent + { + @event.TeamId = teamId; + + return CreateEvent(@event, fromClient); + } + + private T CreateTeamCommand(T command) where T : TeamCommand + { + command.TeamId = teamId; + + return CreateCommand(command); + } + + private Task PublishIdempotentAsync(TeamCommand command) + { + return PublishIdempotentAsync(sut, CreateTeamCommand(command)); + } + + private async Task PublishAsync(TeamCommand command) + { + var actual = await sut.ExecuteAsync(CreateTeamCommand(command), default); + + return actual.Payload; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs new file mode 100644 index 000000000..904935989 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Squidex.Domain.Apps.Entities.Teams.Repositories; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Teams.Indexes +{ + public class TeamsIndexTests + { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly ITeamRepository teamRepository = A.Fake(); + private readonly TeamsIndex sut; + + public TeamsIndexTests() + { + ct = cts.Token; + + sut = new TeamsIndex(teamRepository); + } + + [Fact] + public async Task Should_resolve_teams_by_id() + { + var team = SetupTeam(0); + + A.CallTo(() => teamRepository.QueryAllAsync("user1", ct)) + .Returns(new List { team }); + + var actual = await sut.GetTeamsAsync("user1", ct); + + Assert.Same(actual[0], team); + } + + [Fact] + public async Task Should_return_empty_teams_if_team_not_created() + { + var team = SetupTeam(-1); + + A.CallTo(() => teamRepository.QueryAllAsync("user1", ct)) + .Returns(new List { team }); + + var actual = await sut.GetTeamsAsync("user1", ct); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_resolve_team_by_id() + { + var team = SetupTeam(0); + + A.CallTo(() => teamRepository.FindAsync(team.Id, ct)) + .Returns(team); + + var actual = await sut.GetTeamAsync(team.Id, ct); + + Assert.Same(actual, team); + } + + [Fact] + public async Task Should_return_null_team_if_team_not_created() + { + var team = SetupTeam(0); + + A.CallTo(() => teamRepository.FindAsync(team.Id, ct)) + .Returns(Task.FromResult(null)); + + var actual = await sut.GetTeamAsync(team.Id, ct); + + Assert.Null(actual); + } + + private static ITeamEntity SetupTeam(long version) + { + var team = Mocks.Team(DomainId.NewGuid()); + + A.CallTo(() => team.Version).Returns(version); + + return team; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs index 34a135b0d..83fba2921 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers protected async Task PublishIdempotentAsync(DomainObject domainObject, IAggregateCommand command, CancellationToken ct = default) where T : class, IDomainState, new() { - var result = await domainObject.ExecuteAsync(command, default); + var actual = await domainObject.ExecuteAsync(command, default); var previousSnapshot = domainObject.Snapshot; var previousVersion = domainObject.Snapshot.Version; @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers Assert.Same(previousSnapshot, domainObject.Snapshot); Assert.Equal(previousVersion, domainObject.Snapshot.Version); - return result.Payload; + return actual.Payload; } protected TCommand CreateCommand(TCommand command) where TCommand : SquidexCommand diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs index 8f1f071c7..7fd2d09d2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs @@ -7,10 +7,12 @@ using System.Security.Claims; using FakeItEasy; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Teams; using Squidex.Infrastructure; using Squidex.Infrastructure.Security; using Squidex.Shared; @@ -53,6 +55,18 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers return schema; } + public static ITeamEntity Team(DomainId teamId, string teamName = "my-team", string contributor = "user") + { + var team = A.Fake(); + + A.CallTo(() => team.Id).Returns(teamId); + A.CallTo(() => team.UniqueId).Returns(teamId); + A.CallTo(() => team.Name).Returns(teamName); + A.CallTo(() => team.Contributors).Returns(Contributors.Empty.Assign(contributor, Role.Owner)); + + return team; + } + public static ClaimsPrincipal ApiUser(string? role = null) { return CreateUser(role, "api"); diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs index 440b109d8..b6693b0e5 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs @@ -54,9 +54,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userService.CreateAsync(email, A.That.Matches(x => x.Invited == true), false, ct)) .Returns(user); - var result = await sut.CreateUserIfNotExistsAsync(email, true, ct); + var actual = await sut.CreateUserIfNotExistsAsync(email, true, ct); - Assert.Equal((user, true), result); + Assert.Equal((user, true), actual); } [Fact] @@ -72,9 +72,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userService.FindByEmailAsync(email, ct)) .Returns(user); - var result = await sut.CreateUserIfNotExistsAsync(email, true, ct); + var actual = await sut.CreateUserIfNotExistsAsync(email, true, ct); - Assert.Equal((user, false), result); + Assert.Equal((user, false), actual); } [Fact] @@ -111,9 +111,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userService.FindByEmailAsync(id, ct)) .Returns(user); - var result = await sut.FindByIdOrEmailAsync(id, ct); + var actual = await sut.FindByIdOrEmailAsync(id, ct); - Assert.Equal(user, result); + Assert.Equal(user, actual); } [Fact] @@ -126,9 +126,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userService.FindByIdAsync(id, ct)) .Returns(user); - var result = await sut.FindByIdOrEmailAsync(id, ct); + var actual = await sut.FindByIdOrEmailAsync(id, ct); - Assert.Equal(user, result); + Assert.Equal(user, actual); } [Fact] @@ -141,9 +141,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userService.FindByIdAsync(id, ct)) .Returns(user); - var result = await sut.FindByIdOrEmailAsync(id, ct); + var actual = await sut.FindByIdOrEmailAsync(id, ct); - Assert.Equal(user, result); + Assert.Equal(user, actual); } [Fact] @@ -156,9 +156,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userService.QueryAsync(email, 10, 0, ct)) .Returns(users); - var result = await sut.QueryByEmailAsync(email, ct); + var actual = await sut.QueryByEmailAsync(email, ct); - Assert.Single(result); + Assert.Single(actual); } [Fact] @@ -171,9 +171,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userService.QueryAsync(ids, ct)) .Returns(users); - var result = await sut.QueryManyAsync(ids, ct); + var actual = await sut.QueryManyAsync(ids, ct); - Assert.Single(result); + Assert.Single(actual); } [Fact] @@ -184,9 +184,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userService.QueryAsync(null, int.MaxValue, 0, ct)) .Returns(users); - var result = await sut.QueryAllAsync(ct); + var actual = await sut.QueryAllAsync(ct); - Assert.Single(result); + Assert.Single(actual); } } } diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs index c67fea5b6..1fe1968f5 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs @@ -46,9 +46,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userFactory.IsId(invalidId)) .Returns(false); - var result = await sut.FindByIdAsync(invalidId); + var actual = await sut.FindByIdAsync(invalidId); - Assert.Null(result); + Assert.Null(actual); A.CallTo(() => userManager.FindByIdAsync(invalidId)) .MustNotHaveHappened(); @@ -59,9 +59,9 @@ namespace Squidex.Domain.Users { var identity = CreateIdentity(found: true); - var result = await sut.FindByIdAsync(identity.Id); + var actual = await sut.FindByIdAsync(identity.Id); - Assert.Same(identity, result?.Identity); + Assert.Same(identity, actual?.Identity); } [Fact] @@ -69,9 +69,9 @@ namespace Squidex.Domain.Users { var identity = CreateIdentity(found: false); - var result = await sut.FindByIdAsync(identity.Id); + var actual = await sut.FindByIdAsync(identity.Id); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -79,9 +79,9 @@ namespace Squidex.Domain.Users { var identity = CreateIdentity(found: true); - var result = await sut.FindByEmailAsync(identity.Email); + var actual = await sut.FindByEmailAsync(identity.Email); - Assert.Same(identity, result?.Identity); + Assert.Same(identity, actual?.Identity); } [Fact] @@ -89,9 +89,9 @@ namespace Squidex.Domain.Users { var identity = CreateIdentity(found: false); - var result = await sut.FindByEmailAsync(identity.Email); + var actual = await sut.FindByEmailAsync(identity.Email); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -105,9 +105,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) .Returns(identity); - var result = await sut.FindByLoginAsync(provider, providerKey); + var actual = await sut.FindByLoginAsync(provider, providerKey); - Assert.Same(identity, result?.Identity); + Assert.Same(identity, actual?.Identity); } [Fact] @@ -121,9 +121,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) .Returns(Task.FromResult(null!)); - var result = await sut.FindByLoginAsync(provider, providerKey); + var actual = await sut.FindByLoginAsync(provider, providerKey); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -139,9 +139,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userManager.HasPasswordAsync(identity)) .Returns(true); - var result = await sut.HasPasswordAsync(user); + var actual = await sut.HasPasswordAsync(user); - Assert.True(result); + Assert.True(actual); } [Fact] @@ -159,9 +159,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userManager.GetLoginsAsync(identity)) .Returns(logins); - var result = await sut.GetLoginsAsync(user); + var actual = await sut.GetLoginsAsync(user); - Assert.Same(logins, result); + Assert.Same(logins, actual); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs index d5e7879c2..a97393098 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs @@ -27,10 +27,10 @@ namespace Squidex.Infrastructure.Caching { var sut = new QueryCache(); - var (queried, result) = await ConfigureAsync(sut, 1, 2); + var (queried, actual) = await ConfigureAsync(sut, 1, 2); Assert.Equal(new[] { 1, 2 }, queried); - Assert.Equal(new[] { 1, 2 }, result); + Assert.Equal(new[] { 1, 2 }, actual); } [Fact] @@ -38,14 +38,14 @@ namespace Squidex.Infrastructure.Caching { var sut = new QueryCache(); - var (queried1, result1) = await ConfigureAsync(sut, 1, 2); - var (queried2, result2) = await ConfigureAsync(sut, 1, 2, 3, 4); + var (queried1, actual1) = await ConfigureAsync(sut, 1, 2); + var (queried2, actual2) = await ConfigureAsync(sut, 1, 2, 3, 4); Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); - Assert.Equal(new[] { 1, 2 }, result1.ToArray()); - Assert.Equal(new[] { 1, 2, 3, 4 }, result2.ToArray()); + Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); + Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); } [Fact] @@ -55,10 +55,10 @@ namespace Squidex.Infrastructure.Caching sut.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); - var (queried, result) = await ConfigureAsync(sut, 1, 2, 3, 4); + var (queried, actual) = await ConfigureAsync(sut, 1, 2, 3, 4); Assert.Equal(new[] { 3, 4 }, queried); - Assert.Equal(new[] { 2, 3, 4 }, result); + Assert.Equal(new[] { 2, 3, 4 }, actual); } [Fact] @@ -71,10 +71,10 @@ namespace Squidex.Infrastructure.Caching sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); - var (queried, result) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); + var (queried, actual) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); Assert.Equal(new[] { 3, 4 }, queried); - Assert.Equal(new[] { 2, 3, 4 }, result); + Assert.Equal(new[] { 2, 3, 4 }, actual); } [Fact] @@ -85,10 +85,10 @@ namespace Squidex.Infrastructure.Caching sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); - var (queried, result) = await ConfigureAsync(sut2, 1, 2, 3, 4); + var (queried, actual) = await ConfigureAsync(sut2, 1, 2, 3, 4); Assert.Equal(new[] { 1, 2, 3, 4 }, queried); - Assert.Equal(new[] { 1, 2, 3, 4 }, result); + Assert.Equal(new[] { 1, 2, 3, 4 }, actual); } [Fact] @@ -101,10 +101,10 @@ namespace Squidex.Infrastructure.Caching sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); - var (queried, result) = await ConfigureAsync(sut2, 1, 2, 3, 4); + var (queried, actual) = await ConfigureAsync(sut2, 1, 2, 3, 4); Assert.Equal(new[] { 1, 2, 3, 4 }, queried); - Assert.Equal(new[] { 1, 2, 3, 4 }, result); + Assert.Equal(new[] { 1, 2, 3, 4 }, actual); } [Fact] @@ -112,14 +112,14 @@ namespace Squidex.Infrastructure.Caching { var sut = new QueryCache(); - var (queried1, result1) = await ConfigureAsync(sut, x => x > 1, default, 1, 2); - var (queried2, result2) = await ConfigureAsync(sut, 1, 2, 3, 4); + var (queried1, actual1) = await ConfigureAsync(sut, x => x > 1, default, 1, 2); + var (queried2, actual2) = await ConfigureAsync(sut, 1, 2, 3, 4); Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); - Assert.Equal(new[] { 2 }, result1.ToArray()); - Assert.Equal(new[] { 2, 3, 4 }, result2.ToArray()); + Assert.Equal(new[] { 2 }, actual1.ToArray()); + Assert.Equal(new[] { 2, 3, 4 }, actual2.ToArray()); } [Fact] @@ -130,14 +130,14 @@ namespace Squidex.Infrastructure.Caching var cacheDuration = TimeSpan.FromSeconds(10); - var (queried1, result1) = await ConfigureAsync(sut1, x => true, cacheDuration, 1, 2); - var (queried2, result2) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); + var (queried1, actual1) = await ConfigureAsync(sut1, x => true, cacheDuration, 1, 2); + var (queried2, actual2) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); - Assert.Equal(new[] { 1, 2 }, result1.ToArray()); - Assert.Equal(new[] { 1, 2, 3, 4 }, result2.ToArray()); + Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); + Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); } [Fact] @@ -146,14 +146,14 @@ namespace Squidex.Infrastructure.Caching var sut1 = new QueryCache(memoryCache); var sut2 = new QueryCache(memoryCache); - var (queried1, result1) = await ConfigureAsync(sut1, x => true, null, 1, 2); - var (queried2, result2) = await ConfigureAsync(sut2, x => true, null, 1, 2, 3, 4); + var (queried1, actual1) = await ConfigureAsync(sut1, x => true, null, 1, 2); + var (queried2, actual2) = await ConfigureAsync(sut2, x => true, null, 1, 2, 3, 4); Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); Assert.Equal(new[] { 1, 2, 3, 4 }, queried2.ToArray()); - Assert.Equal(new[] { 1, 2 }, result1.ToArray()); - Assert.Equal(new[] { 1, 2, 3, 4 }, result2.ToArray()); + Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); + Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); } private static Task<(int[], int[])> ConfigureAsync(IQueryCache sut, params int[] ids) @@ -165,7 +165,7 @@ namespace Squidex.Infrastructure.Caching { var queried = new HashSet(); - var result = await sut.CacheOrQueryAsync(ids, async pending => + var actual = await sut.CacheOrQueryAsync(ids, async pending => { queried.AddRange(pending); @@ -174,7 +174,7 @@ namespace Squidex.Infrastructure.Caching return pending.Where(predicate).Select(x => new CachedEntry(x)); }, cacheDuration); - return (queried.ToArray(), result.Select(x => x.Value).ToArray()); + return (queried.ToArray(), actual.Select(x => x.Value).ToArray()); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs index e5abfafcb..c9a5b1832 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs @@ -302,18 +302,18 @@ namespace Squidex.Infrastructure.Collections sut.Add(1, 10); sut.Add(2, 20); - var result = new List>(); + var actual = new List>(); foreach (var entry in sut) { - result.Add(entry); + actual.Add(entry); } Assert.Equal(new[] { new KeyValuePair(1, 10), new KeyValuePair(2, 20) - }, result.ToArray()); + }, actual.ToArray()); } [Fact] @@ -324,18 +324,18 @@ namespace Squidex.Infrastructure.Collections sut.Add(1, 10); sut.Add(2, 20); - var result = new List>(); + var actual = new List>(); foreach (KeyValuePair entry in (IEnumerable)sut) { - result.Add(entry); + actual.Add(entry); } Assert.Equal(new[] { new KeyValuePair(1, 10), new KeyValuePair(2, 20) - }, result.ToArray()); + }, actual.ToArray()); } [Fact] @@ -361,14 +361,14 @@ namespace Squidex.Infrastructure.Collections sut.Add(1, 10); sut.Add(2, 20); - var result = new List(); + var actual = new List(); foreach (var entry in sut.Keys) { - result.Add(entry); + actual.Add(entry); } - Assert.Equal(new[] { 1, 2 }, result.ToArray()); + Assert.Equal(new[] { 1, 2 }, actual.ToArray()); } [Fact] @@ -379,14 +379,14 @@ namespace Squidex.Infrastructure.Collections sut.Add(1, 10); sut.Add(2, 20); - var result = new List(); + var actual = new List(); foreach (int entry in (IEnumerable)sut.Keys) { - result.Add(entry); + actual.Add(entry); } - Assert.Equal(new[] { 1, 2 }, result.ToArray()); + Assert.Equal(new[] { 1, 2 }, actual.ToArray()); } [Fact] @@ -408,14 +408,14 @@ namespace Squidex.Infrastructure.Collections sut.Add(1, 10); sut.Add(2, 20); - var result = new List(); + var actual = new List(); foreach (var entry in sut.Values) { - result.Add(entry); + actual.Add(entry); } - Assert.Equal(new[] { 10, 20 }, result.ToArray()); + Assert.Equal(new[] { 10, 20 }, actual.ToArray()); } [Fact] @@ -426,14 +426,14 @@ namespace Squidex.Infrastructure.Collections sut.Add(1, 10); sut.Add(2, 20); - var result = new List(); + var actual = new List(); foreach (int entry in (IEnumerable)sut.Values) { - result.Add(entry); + actual.Add(entry); } - Assert.Equal(new[] { 10, 20 }, result.ToArray()); + Assert.Equal(new[] { 10, 20 }, actual.ToArray()); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs index 247e6f415..e634ff930 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs @@ -23,25 +23,25 @@ namespace Squidex.Infrastructure.Collections [Fact] public void Should_return_empty_instance_for_empty_source() { - var result = new Dictionary().ToReadonlyDictionary(); + var actual = new Dictionary().ToReadonlyDictionary(); - Assert.Same(ReadonlyDictionary.Empty(), result); + Assert.Same(ReadonlyDictionary.Empty(), actual); } [Fact] public void Should_return_empty_instance_for_empty_source_and_key_selector() { - var result = Enumerable.Empty().ToReadonlyDictionary(x => x); + var actual = Enumerable.Empty().ToReadonlyDictionary(x => x); - Assert.Same(ReadonlyDictionary.Empty(), result); + Assert.Same(ReadonlyDictionary.Empty(), actual); } [Fact] public void Should_return_empty_instance_for_empty_source_and_value_selector() { - var result = Enumerable.Empty().ToReadonlyDictionary(x => x, x => x); + var actual = Enumerable.Empty().ToReadonlyDictionary(x => x, x => x); - Assert.Same(ReadonlyDictionary.Empty(), result); + Assert.Same(ReadonlyDictionary.Empty(), actual); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs index 3034352e7..80a07b39f 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs @@ -23,25 +23,25 @@ namespace Squidex.Infrastructure.Collections [Fact] public void Should_return_empty_instance_for_empty_array() { - var result = ReadonlyList.Create(); + var actual = ReadonlyList.Create(); - Assert.Same(ReadonlyList.Empty(), result); + Assert.Same(ReadonlyList.Empty(), actual); } [Fact] public void Should_return_empty_instance_for_null_array() { - var result = ReadonlyList.Create((int[]?)null); + var actual = ReadonlyList.Create((int[]?)null); - Assert.Same(ReadonlyList.Empty(), result); + Assert.Same(ReadonlyList.Empty(), actual); } [Fact] public void Should_return_empty_instance_for_empty_enumerable() { - var result = Enumerable.Empty().ToReadonlyList(); + var actual = Enumerable.Empty().ToReadonlyList(); - Assert.Same(ReadonlyList.Empty(), result); + Assert.Same(ReadonlyList.Empty(), actual); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs index 14e7243ea..f81e76824 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs @@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public void Should_provide_result_if_succeeded_with_value() + public void Should_provide_actual_if_succeeded_with_value() { sut.Complete("RESULT"); @@ -51,7 +51,7 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public void Should_provide_plain_result_if_succeeded_with_value() + public void Should_provide_plain_actual_if_succeeded_with_value() { sut.Complete("RESULT"); diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs index 628b61fed..0d2e58ed3 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs @@ -66,9 +66,9 @@ namespace Squidex.Infrastructure.Commands .Returns(true) .AssignsOutAndRefParameters(20); - var result = await sut.GetAsync(id, 10, ct); + var actual = await sut.GetAsync(id, 10, ct); - Assert.Equal(20, result); + Assert.Equal(20, actual); } [Fact] @@ -77,9 +77,9 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => serializer.Deserialize(A._, null, false)) .Returns(20); - var result = await sut.GetAsync(id, 10, ct); + var actual = await sut.GetAsync(id, 10, ct); - Assert.Equal(20, result); + Assert.Equal(20, actual); A.CallTo(() => distributedCache.GetAsync($"{id}_10", ct)) .MustHaveHappened(); diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs index 5806876eb..6eeba54ac 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs @@ -68,7 +68,7 @@ namespace Squidex.Infrastructure.Commands [Fact] public async Task Should_write_state_and_events_if_created() { - var result = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); + var actual = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); A.CallTo(() => state.Persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4), default)) .MustHaveHappened(); @@ -79,7 +79,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => state.Persistence.ReadAsync(A._, A._)) .MustNotHaveHappened(); - Assert.Equal(CommandResult.Empty(id, 0, EtagVersion.Empty), result); + Assert.Equal(CommandResult.Empty(id, 0, EtagVersion.Empty), actual); Assert.Equal(0, sut.Version); Assert.Equal(0, sut.Snapshot.Version); @@ -174,7 +174,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => state.Persistence.WriteEventsAsync(A>>._, ct)) .Throws(new InconsistentStateException(2, -1)).Once(); - var result = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); + var actual = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); A.CallTo(() => state.Persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), ct)) .MustHaveHappenedANumberOfTimesMatching(x => x == 3); @@ -182,7 +182,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => state.Persistence.ReadAsync(A._, ct)) .MustHaveHappened(); - Assert.Equal(CommandResult.Empty(id, 2, 1), result); + Assert.Equal(CommandResult.Empty(id, 2, 1), actual); Assert.Equal(2, sut.Version); Assert.Equal(2, sut.Snapshot.Version); @@ -216,7 +216,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => state.Persistence.WriteEventsAsync(A>>._, ct)) .Throws(new InconsistentStateException(2, -1)).Once(); - var result = await sut.ExecuteAsync(new Upsert { Value = 4 }, ct); + var actual = await sut.ExecuteAsync(new Upsert { Value = 4 }, ct); A.CallTo(() => state.Persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), ct)) .MustHaveHappenedANumberOfTimesMatching(x => x == 3); @@ -224,7 +224,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => state.Persistence.ReadAsync(A._, ct)) .MustHaveHappened(); - Assert.Equal(CommandResult.Empty(id, 2, 1), result); + Assert.Equal(CommandResult.Empty(id, 2, 1), actual); Assert.Equal(2, sut.Version); Assert.Equal(2, sut.Snapshot.Version); @@ -251,7 +251,7 @@ namespace Squidex.Infrastructure.Commands { await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); - var result = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); + var actual = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); A.CallTo(() => state.Persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8), default)) .MustHaveHappened(); @@ -262,7 +262,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => state.Persistence.ReadAsync(A._, A._)) .MustNotHaveHappened(); - Assert.Equal(CommandResult.Empty(id, 1, 0), result); + Assert.Equal(CommandResult.Empty(id, 1, 0), actual); Assert.Equal(1, sut.Version); Assert.Equal(1, sut.Snapshot.Version); @@ -275,7 +275,7 @@ namespace Squidex.Infrastructure.Commands { SetupCreated(4); - var result = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); + var actual = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); A.CallTo(() => state.Persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8), default)) .MustHaveHappened(); @@ -286,7 +286,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => state.Persistence.ReadAsync(A._, ct)) .MustHaveHappenedOnceExactly(); - Assert.Equal(CommandResult.Empty(id, 1, 0), result); + Assert.Equal(CommandResult.Empty(id, 1, 0), actual); Assert.Equal(1, sut.Version); Assert.Equal(1, sut.Snapshot.Version); @@ -362,11 +362,11 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public async Task Should_return_custom_result_on_create() + public async Task Should_return_custom_actual_on_create() { - var result = await sut.ExecuteAsync(new CreateCustom(), ct); + var actual = await sut.ExecuteAsync(new CreateCustom(), ct); - Assert.Equal(new CommandResult(id, 0, EtagVersion.Empty, "CREATED"), result); + Assert.Equal(new CommandResult(id, 0, EtagVersion.Empty, "CREATED"), actual); } [Fact] @@ -384,13 +384,13 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public async Task Should_return_custom_result_on_update() + public async Task Should_return_custom_actual_on_update() { SetupCreated(4); - var result = await sut.ExecuteAsync(new UpdateCustom(), ct); + var actual = await sut.ExecuteAsync(new UpdateCustom(), ct); - Assert.Equal(new CommandResult(id, 1, 0, "UPDATED"), result); + Assert.Equal(new CommandResult(id, 1, 0, "UPDATED"), actual); } [Fact] @@ -406,9 +406,9 @@ namespace Squidex.Infrastructure.Commands { SetupCreated(4); - var result = await sut.ExecuteAsync(new UpdateAuto { Value = MyDomainState.Unchanged }, ct); + var actual = await sut.ExecuteAsync(new UpdateAuto { Value = MyDomainState.Unchanged }, ct); - Assert.Equal(CommandResult.Empty(id, 0, 0), result); + Assert.Equal(CommandResult.Empty(id, 0, 0), actual); Assert.Equal(0, sut.Version); Assert.Equal(0, sut.Snapshot.Version); diff --git a/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs b/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs index 9e3421e90..e64af0cea 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs @@ -61,9 +61,9 @@ namespace Squidex.Infrastructure { var text = "123"; - var result = typeConverter.ConvertFromString(text); + var actual = typeConverter.ConvertFromString(text); - Assert.Equal(DomainId.Create(text), result); + Assert.Equal(DomainId.Create(text), actual); } [Fact] @@ -71,9 +71,9 @@ namespace Squidex.Infrastructure { var guid = Guid.NewGuid(); - var result = typeConverter.ConvertFrom(guid); + var actual = typeConverter.ConvertFrom(guid); - Assert.Equal(guid.ToString(), result?.ToString()); + Assert.Equal(guid.ToString(), actual?.ToString()); } [Fact] @@ -81,9 +81,9 @@ namespace Squidex.Infrastructure { var text = "123"; - var result = typeConverter.ConvertToString(DomainId.Create(text)); + var actual = typeConverter.ConvertToString(DomainId.Create(text)); - Assert.Equal(text, result); + Assert.Equal(text, actual); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs index cae21b465..93467d196 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs @@ -16,42 +16,42 @@ namespace Squidex.Infrastructure public void Should_serialize_and_deserialize_DomainException() { var source = new DomainException("Message", "ErrorCode"); - var result = source.SerializeAndDeserializeBinary(); + var actual = source.SerializeAndDeserializeBinary(); - Assert.Equal(result.ErrorCode, source.ErrorCode); - Assert.Equal(result.Message, source.Message); + Assert.Equal(actual.ErrorCode, source.ErrorCode); + Assert.Equal(actual.Message, source.Message); } [Fact] public void Should_serialize_and_deserialize_DomainObjectDeletedException() { var source = new DomainObjectDeletedException("123"); - var result = source.SerializeAndDeserializeBinary(); + var actual = source.SerializeAndDeserializeBinary(); - Assert.Equal(result.Id, source.Id); - Assert.Equal(result.Message, source.Message); + Assert.Equal(actual.Id, source.Id); + Assert.Equal(actual.Message, source.Message); } [Fact] public void Should_serialize_and_deserialize_DomainObjectNotFoundException() { var source = new DomainObjectNotFoundException("123"); - var result = source.SerializeAndDeserializeBinary(); + var actual = source.SerializeAndDeserializeBinary(); - Assert.Equal(result.Id, source.Id); - Assert.Equal(result.Message, source.Message); + Assert.Equal(actual.Id, source.Id); + Assert.Equal(actual.Message, source.Message); } [Fact] public void Should_serialize_and_deserialize_DomainObjectVersionExceptionn() { var source = new DomainObjectVersionException("123", 100, 200); - var result = source.SerializeAndDeserializeBinary(); + var actual = source.SerializeAndDeserializeBinary(); - Assert.Equal(result.Id, source.Id); - Assert.Equal(result.ExpectedVersion, source.ExpectedVersion); - Assert.Equal(result.CurrentVersion, source.CurrentVersion); - Assert.Equal(result.Message, source.Message); + Assert.Equal(actual.Id, source.Id); + Assert.Equal(actual.ExpectedVersion, source.ExpectedVersion); + Assert.Equal(actual.CurrentVersion, source.CurrentVersion); + Assert.Equal(actual.Message, source.Message); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs index 9544d47e9..8b60c0a92 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs @@ -64,9 +64,9 @@ namespace Squidex.Infrastructure.EventSourcing.Consume }, 2) }.ToAsyncEnumerable()); - var result = await sut.GetConsumersAsync(default); + var actual = await sut.GetConsumersAsync(default); - result.Should().BeEquivalentTo( + actual.Should().BeEquivalentTo( new List { new EventConsumerInfo { Name = consumerName1, Position = "1" }, diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs index a8f01091c..4fff4009f 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs @@ -16,12 +16,12 @@ namespace Squidex.Infrastructure.EventSourcing public void Should_serialize_and_deserialize() { var source = new WrongEventVersionException(100, 200); - var result = source.SerializeAndDeserializeBinary(); + var actual = source.SerializeAndDeserializeBinary(); - Assert.Equal(result.ExpectedVersion, source.ExpectedVersion); - Assert.Equal(result.CurrentVersion, source.CurrentVersion); + Assert.Equal(actual.ExpectedVersion, source.ExpectedVersion); + Assert.Equal(actual.CurrentVersion, source.CurrentVersion); - Assert.Equal(result.Message, source.Message); + Assert.Equal(actual.Message, source.Message); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs index 9223dd889..76eb29c0d 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs @@ -274,12 +274,12 @@ namespace Squidex.Infrastructure.Json.Objects id }; - var result = id.ToString(); + var actual = id.ToString(); foreach (var json in jsons) { - Assert.Equal(result, json.Value); - Assert.Equal(result, json.AsString); + Assert.Equal(actual, json.Value); + Assert.Equal(actual, json.AsString); Assert.Equal(JsonValueType.String, json.Type); Assert.Throws(() => json.AsBoolean); @@ -305,12 +305,12 @@ namespace Squidex.Infrastructure.Json.Objects input }; - var result = new JsonArray { 1, 2 }; + var actual = new JsonArray { 1, 2 }; foreach (var json in jsons) { - Assert.Equal(result, json.Value); - Assert.Equal(result, json.AsArray); + Assert.Equal(actual, json.Value); + Assert.Equal(actual, json.AsArray); Assert.Equal(JsonValueType.Array, json.Type); Assert.Throws(() => json.AsBoolean); @@ -334,12 +334,12 @@ namespace Squidex.Infrastructure.Json.Objects input }; - var result = new JsonObject().Add("1", 1).Add("2", 2); + var actual = new JsonObject().Add("1", 1).Add("2", 2); foreach (var json in jsons) { - Assert.Equal(result, json.Value); - Assert.Equal(result, json.AsObject); + Assert.Equal(actual, json.Value); + Assert.Equal(actual, json.AsObject); Assert.Equal(JsonValueType.Object, json.Type); Assert.Throws(() => json.AsBoolean); @@ -430,10 +430,10 @@ namespace Squidex.Infrastructure.Json.Objects { var json = JsonValue.Null; - var found = json.TryGetByPath("path", out var result); + var found = json.TryGetByPath("path", out var actual); Assert.False(found); - Assert.Equal(default, result); + Assert.Equal(default, actual); } [Fact] @@ -441,10 +441,10 @@ namespace Squidex.Infrastructure.Json.Objects { var json = JsonValue.Create("string"); - var found = json.TryGetByPath("path", out var result); + var found = json.TryGetByPath("path", out var actual); Assert.False(found); - Assert.Equal(default, result); + Assert.Equal(default, actual); } [Fact] @@ -452,10 +452,10 @@ namespace Squidex.Infrastructure.Json.Objects { var json = JsonValue.True; - var found = json.TryGetByPath("path", out var result); + var found = json.TryGetByPath("path", out var actual); Assert.False(found); - Assert.Equal(default, result); + Assert.Equal(default, actual); } [Fact] @@ -463,10 +463,10 @@ namespace Squidex.Infrastructure.Json.Objects { var json = JsonValue.Create(12); - var found = json.TryGetByPath("path", out var result); + var found = json.TryGetByPath("path", out var actual); Assert.False(found); - Assert.Equal(default, result); + Assert.Equal(default, actual); } [Fact] @@ -474,10 +474,10 @@ namespace Squidex.Infrastructure.Json.Objects { JsonValue json = new JsonObject().Add("property", 12); - var found = json.TryGetByPath((string?)null, out var result); + var found = json.TryGetByPath((string?)null, out var actual); Assert.False(found); - Assert.Equal(json, result); + Assert.Equal(json, actual); } [Fact] @@ -485,10 +485,10 @@ namespace Squidex.Infrastructure.Json.Objects { JsonValue json = new JsonObject().Add("property", 12); - var found = json.TryGetByPath(string.Empty, out var result); + var found = json.TryGetByPath(string.Empty, out var actual); Assert.False(found); - Assert.Equal(json, result); + Assert.Equal(json, actual); } [Fact] @@ -502,10 +502,10 @@ namespace Squidex.Infrastructure.Json.Objects new JsonObject() .Add("nested", 13))); - var found = json.TryGetByPath("property[1].nested", out var result); + var found = json.TryGetByPath("property[1].nested", out var actual); Assert.True(found); - Assert.Equal(JsonValue.Create(13), result); + Assert.Equal(JsonValue.Create(13), actual); } [Fact] @@ -515,10 +515,10 @@ namespace Squidex.Infrastructure.Json.Objects new JsonObject() .Add("property", 12); - var found = json.TryGetByPath("notfound", out var result); + var found = json.TryGetByPath("notfound", out var actual); Assert.False(found); - Assert.Equal(default, result); + Assert.Equal(default, actual); } [Fact] @@ -526,10 +526,10 @@ namespace Squidex.Infrastructure.Json.Objects { JsonValue json = JsonValue.Array(12, 14); - var found = json.TryGetByPath("-1", out var result); + var found = json.TryGetByPath("-1", out var actual); Assert.False(found); - Assert.Equal(default, result); + Assert.Equal(default, actual); } [Fact] @@ -537,10 +537,10 @@ namespace Squidex.Infrastructure.Json.Objects { JsonValue json = JsonValue.Array(12, 14); - var found = json.TryGetByPath("2", out var result); + var found = json.TryGetByPath("2", out var actual); Assert.False(found); - Assert.Equal(default, result); + Assert.Equal(default, actual); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs index 9d9da1638..af204e5d3 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs @@ -29,25 +29,25 @@ namespace Squidex.Infrastructure [Fact] public void Should_provide_custom_language() { - var result = Language.GetLanguage("xy"); + var actual = Language.GetLanguage("xy"); - Assert.Equal("xy", result.Iso2Code); + Assert.Equal("xy", actual.Iso2Code); } [Fact] public void Should_trim_custom_language() { - var result = Language.GetLanguage("xy "); + var actual = Language.GetLanguage("xy "); - Assert.Equal("xy", result.Iso2Code); + Assert.Equal("xy", actual.Iso2Code); } [Fact] public void Should_provide_default_language() { - var result = Language.GetLanguage("de"); + var actual = Language.GetLanguage("de"); - Assert.Same(Language.DE, result); + Assert.Same(Language.DE, actual); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs index b93c45179..33c89b8ac 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs @@ -75,7 +75,7 @@ namespace Squidex.Infrastructure.Log } [Fact] - public async Task Should_provide_results_from_repository() + public async Task Should_provide_actuals_from_repository() { var key = "my-key"; @@ -85,13 +85,13 @@ namespace Squidex.Infrastructure.Log A.CallTo(() => requestLogRepository.QueryAllAsync(key, dateFrom, dateTo, ct)) .Returns(AsyncEnumerable.Repeat(new Request { Key = key }, 1)); - var results = await sut.QueryAllAsync(key, dateFrom, dateTo, ct).ToListAsync(ct); + var actuals = await sut.QueryAllAsync(key, dateFrom, dateTo, ct).ToListAsync(ct); - Assert.NotEmpty(results); + Assert.NotEmpty(actuals); } [Fact] - public async Task Should_not_provide_results_from_repository_if_disabled() + public async Task Should_not_provide_actuals_from_repository_if_disabled() { options.StoreEnabled = false; @@ -100,9 +100,9 @@ namespace Squidex.Infrastructure.Log var dateFrom = DateTime.Today; var dateTo = dateFrom.AddDays(4); - var results = await sut.QueryAllAsync(key, dateFrom, dateTo, ct).ToListAsync(ct); + var actuals = await sut.QueryAllAsync(key, dateFrom, dateTo, ct).ToListAsync(ct); - Assert.Empty(results); + Assert.Empty(actuals); A.CallTo(() => requestLogRepository.QueryAllAsync(key, dateFrom, dateTo, A._)) .MustNotHaveHappened(); diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs index 1cff8eedb..30ad1ef30 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs @@ -86,7 +86,7 @@ namespace Squidex.Infrastructure.MongoDb public static TestObject CreateWithValues(bool nested = true) { - var result = new TestObject + var actual = new TestObject { Bool = true, Byte = 0x2, @@ -110,11 +110,11 @@ namespace Squidex.Infrastructure.MongoDb if (nested) { - result.Nested = CreateWithValues(false); - result.NestedArray = Enumerable.Repeat(0, 4).Select(x => CreateWithValues(false)).ToArray(); + actual.Nested = CreateWithValues(false); + actual.NestedArray = Enumerable.Repeat(0, 4).Select(x => CreateWithValues(false)).ToArray(); } - return result; + return actual; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs index 44739ed57..b8e0d8dff 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs @@ -39,9 +39,9 @@ namespace Squidex.Infrastructure.MongoDb var source = new IdEntity { Id = id.ToString() }; - var result = SerializeAndDeserializeBson, IdEntity>(source); + var actual = SerializeAndDeserializeBson, IdEntity>(source); - Assert.Equal(result.Id.ToString(), id.ToString()); + Assert.Equal(actual.Id.ToString(), id.ToString()); } [Fact] @@ -51,9 +51,9 @@ namespace Squidex.Infrastructure.MongoDb var source = new StringEntity { Id = id }; - var result = SerializeAndDeserializeBson, IdEntity>(source); + var actual = SerializeAndDeserializeBson, IdEntity>(source); - Assert.Equal(result.Id.ToString(), id.ToString()); + Assert.Equal(actual.Id.ToString(), id.ToString()); } [Fact] @@ -63,9 +63,9 @@ namespace Squidex.Infrastructure.MongoDb var source = new IdEntity { Id = id }; - var result = SerializeAndDeserializeBson, IdEntity>(source); + var actual = SerializeAndDeserializeBson, IdEntity>(source); - Assert.Equal(result.Id.ToString(), id.ToString()); + Assert.Equal(actual.Id.ToString(), id.ToString()); } private static TOut SerializeAndDeserializeBson(TIn source) diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs index 8ce8494bf..4a46da085 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs @@ -104,9 +104,9 @@ namespace Squidex.Infrastructure.MongoDb using (var reader = new BsonBinaryReader(stream)) { - var result = BsonSerializer.Deserialize(reader); + var actual = BsonSerializer.Deserialize(reader); - return result; + return actual; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs index afe253aba..5001788f1 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs @@ -15,18 +15,18 @@ namespace Squidex.Infrastructure.Queries public void Should_convert_property() { var source = ClrFilter.Eq("property", 1); - var result = PascalCasePathConverter.Transform(source); + var actual = PascalCasePathConverter.Transform(source); - Assert.Equal("Property == 1", result!.ToString()); + Assert.Equal("Property == 1", actual!.ToString()); } [Fact] public void Should_convert_properties() { var source = ClrFilter.Eq("root.child", 1); - var result = PascalCasePathConverter.Transform(source); + var actual = PascalCasePathConverter.Transform(source); - Assert.Equal("Root.Child == 1", result!.ToString()); + Assert.Equal("Root.Child == 1", actual!.ToString()); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs index 8905236ad..60ed820ed 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs @@ -16,9 +16,9 @@ namespace Squidex.Infrastructure.Queries { var source = ClrFilter.Or(ClrFilter.Eq("path", 2), ClrFilter.Eq("path", 3)); - var result = Optimizer.Optimize(source); + var actual = Optimizer.Optimize(source); - Assert.Equal("(path == 2 || path == 3)", result!.ToString()); + Assert.Equal("(path == 2 || path == 3)", actual!.ToString()); } [Fact] @@ -26,9 +26,9 @@ namespace Squidex.Infrastructure.Queries { var source = ClrFilter.And(ClrFilter.Eq("path", 1), ClrFilter.Or()); - var result = Optimizer.Optimize(source); + var actual = Optimizer.Optimize(source); - Assert.Equal("path == 1", result!.ToString()); + Assert.Equal("path == 1", actual!.ToString()); } [Fact] @@ -36,9 +36,9 @@ namespace Squidex.Infrastructure.Queries { var source = ClrFilter.And(ClrFilter.And()); - var result = Optimizer.Optimize(source); + var actual = Optimizer.Optimize(source); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -46,9 +46,9 @@ namespace Squidex.Infrastructure.Queries { var source = ClrFilter.And(); - var result = Optimizer.Optimize(source); + var actual = Optimizer.Optimize(source); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -56,9 +56,9 @@ namespace Squidex.Infrastructure.Queries { var source = ClrFilter.Not(ClrFilter.And()); - var result = Optimizer.Optimize(source); + var actual = Optimizer.Optimize(source); - Assert.Null(result); + Assert.Null(actual); } [Fact] @@ -66,9 +66,9 @@ namespace Squidex.Infrastructure.Queries { var source = ClrFilter.Not(ClrFilter.Eq("path", 1)); - var result = Optimizer.Optimize(source); + var actual = Optimizer.Optimize(source); - Assert.Equal("path != 1", result!.ToString()); + Assert.Equal("path != 1", actual!.ToString()); } [Fact] @@ -76,9 +76,9 @@ namespace Squidex.Infrastructure.Queries { var source = ClrFilter.Not(ClrFilter.Ne("path", 1)); - var result = Optimizer.Optimize(source); + var actual = Optimizer.Optimize(source); - Assert.Equal("path == 1", result!.ToString()); + Assert.Equal("path == 1", actual!.ToString()); } [Fact] @@ -86,9 +86,9 @@ namespace Squidex.Infrastructure.Queries { var source = ClrFilter.Not(ClrFilter.Lt("path", 1)); - var result = Optimizer.Optimize(source); + var actual = Optimizer.Optimize(source); - Assert.Equal("!(path < 1)", result!.ToString()); + Assert.Equal("!(path < 1)", actual!.ToString()); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs index a46dddd46..f8a4bbd99 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs @@ -16,16 +16,16 @@ namespace Squidex.Infrastructure.States public void Should_serialize_and_deserialize() { var source = new InconsistentStateException(100, 200, new InvalidOperationException("Inner")); - var result = source.SerializeAndDeserializeBinary(); + var actual = source.SerializeAndDeserializeBinary(); - Assert.IsType(result.InnerException); + Assert.IsType(actual.InnerException); - Assert.Equal("Inner", result.InnerException?.Message); + Assert.Equal("Inner", actual.InnerException?.Message); - Assert.Equal(result.VersionExpected, source.VersionExpected); - Assert.Equal(result.VersionCurrent, source.VersionCurrent); + Assert.Equal(actual.VersionExpected, source.VersionExpected); + Assert.Equal(actual.VersionCurrent, source.VersionCurrent); - Assert.Equal(result.Message, source.Message); + Assert.Equal(actual.Message, source.Message); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/SimpleStateTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/SimpleStateTests.cs index 6e2054664..780f2498c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/SimpleStateTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/SimpleStateTests.cs @@ -97,9 +97,9 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_state_on_update_and_return_when_callback_returns_true() { - var result = await sut.UpdateAsync(x => (true, 42), ct: ct); + var actual = await sut.UpdateAsync(x => (true, 42), ct: ct); - Assert.Equal(42, result); + Assert.Equal(42, actual); A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) .MustHaveHappened(); @@ -108,9 +108,9 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_write_state_on_update_and_return_when_callback_returns_false() { - var result = await sut.UpdateAsync(x => (false, 42), ct: ct); + var actual = await sut.UpdateAsync(x => (false, 42), ct: ct); - Assert.Equal(42, result); + Assert.Equal(42, actual); A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) .MustNotHaveHappened(); diff --git a/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs index 1f5eee93c..fc3d5eb47 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs @@ -46,9 +46,9 @@ namespace Squidex.Infrastructure [InlineData("http://squidex.io/base/", "/path/to/res", true, "http://squidex.io/base/path/to/res/")] public void Should_provide_full_url_without_query_or_fragment(string baseUrl, string path, bool trailingSlash, string output) { - var result = baseUrl.BuildFullUrl(path, trailingSlash); + var actual = baseUrl.BuildFullUrl(path, trailingSlash); - Assert.Equal(output, result); + Assert.Equal(output, actual); } [Theory] @@ -57,49 +57,49 @@ namespace Squidex.Infrastructure [InlineData("http://squidex.io/base/", "path/to/res;query=1", true, "http://squidex.io/base/path/to/res;query=1")] public void Should_provide_full_url_wit_query_or_fragment(string baseUrl, string path, bool trailingSlash, string output) { - var result = baseUrl.BuildFullUrl(path, trailingSlash); + var actual = baseUrl.BuildFullUrl(path, trailingSlash); - Assert.Equal(output, result); + Assert.Equal(output, actual); } [Fact] public void Should_join_non_empty_if_all_are_valid() { - var result = StringExtensions.JoinNonEmpty("_", "1", "2", "3"); + var actual = StringExtensions.JoinNonEmpty("_", "1", "2", "3"); - Assert.Equal("1_2_3", result); + Assert.Equal("1_2_3", actual); } [Fact] public void Should_join_non_empty_if_first_invalid() { - var result = StringExtensions.JoinNonEmpty("_", null, "2", "3"); + var actual = StringExtensions.JoinNonEmpty("_", null, "2", "3"); - Assert.Equal("2_3", result); + Assert.Equal("2_3", actual); } [Fact] public void Should_join_non_empty_if_middle_invalid() { - var result = StringExtensions.JoinNonEmpty("_", "1", null, "3"); + var actual = StringExtensions.JoinNonEmpty("_", "1", null, "3"); - Assert.Equal("1_3", result); + Assert.Equal("1_3", actual); } [Fact] public void Should_join_non_empty_if_last_invalid() { - var result = StringExtensions.JoinNonEmpty("_", "1", "2", null); + var actual = StringExtensions.JoinNonEmpty("_", "1", "2", null); - Assert.Equal("1_2", result); + Assert.Equal("1_2", actual); } [Fact] public void Should_escape_json() { - var result = StringExtensions.JsonEscape("Hello \"World\""); + var actual = StringExtensions.JsonEscape("Hello \"World\""); - Assert.Equal("Hello \\\"World\\\"", result); + Assert.Equal("Hello \\\"World\\\"", actual); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs index 2783c4b6d..cfdd45a62 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs @@ -12,7 +12,7 @@ namespace Squidex.Infrastructure.Tasks { public class SchedulerTests { - private readonly ConcurrentBag results = new ConcurrentBag(); + private readonly ConcurrentBag actuals = new ConcurrentBag(); private readonly Scheduler sut = new Scheduler(); [Fact] @@ -22,7 +22,7 @@ namespace Squidex.Infrastructure.Tasks await sut.CompleteAsync(); - Assert.Equal(new[] { 1 }, results.ToArray()); + Assert.Equal(new[] { 1 }, actuals.ToArray()); } [Fact] @@ -37,7 +37,7 @@ namespace Squidex.Infrastructure.Tasks await limited.CompleteAsync(); - Assert.Equal(Enumerable.Range(1, 10).ToArray(), results.OrderBy(x => x).ToArray()); + Assert.Equal(Enumerable.Range(1, 10).ToArray(), actuals.OrderBy(x => x).ToArray()); } [Fact] @@ -48,7 +48,7 @@ namespace Squidex.Infrastructure.Tasks await sut.CompleteAsync(); - Assert.Equal(new[] { 1, 2 }, results.OrderBy(x => x).ToArray()); + Assert.Equal(new[] { 1, 2 }, actuals.OrderBy(x => x).ToArray()); } [Fact] @@ -58,13 +58,13 @@ namespace Squidex.Infrastructure.Tasks { await Task.Delay(1); - results.Add(1); + actuals.Add(1); sut.Schedule(async _ => { await Task.Delay(1); - results.Add(2); + actuals.Add(2); Schedule(3); }); @@ -72,7 +72,7 @@ namespace Squidex.Infrastructure.Tasks await sut.CompleteAsync(); - Assert.Equal(new[] { 1, 2, 3 }, results.OrderBy(x => x).ToArray()); + Assert.Equal(new[] { 1, 2, 3 }, actuals.OrderBy(x => x).ToArray()); } [Fact] @@ -86,7 +86,7 @@ namespace Squidex.Infrastructure.Tasks await Task.Delay(50); - Assert.Equal(new[] { 1 }, results.OrderBy(x => x).ToArray()); + Assert.Equal(new[] { 1 }, actuals.OrderBy(x => x).ToArray()); } private void Schedule(int value) @@ -95,7 +95,7 @@ namespace Squidex.Infrastructure.Tasks { await Task.Delay(1); - results.Add(value); + actuals.Add(value); }); } @@ -105,7 +105,7 @@ namespace Squidex.Infrastructure.Tasks { await Task.Delay(1); - results.Add(value); + actuals.Add(value); }); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Translations/TTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Translations/TTests.cs index b69f19ad8..984345c4b 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Translations/TTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Translations/TTests.cs @@ -22,41 +22,41 @@ namespace Squidex.Infrastructure.Translations [Fact] public void Should_return_key_if_not_found() { - var result = sut.Get(CultureInfo.CurrentUICulture, "key", "fallback"); + var actual = sut.Get(CultureInfo.CurrentUICulture, "key", "fallback"); - Assert.Equal(("fallback", false), result); + Assert.Equal(("fallback", false), actual); } [Fact] public void Should_return_simple_key() { - var result = sut.Get(CultureInfo.CurrentUICulture, "simple", "fallback"); + var actual = sut.Get(CultureInfo.CurrentUICulture, "simple", "fallback"); - Assert.Equal(("Simple Result", true), result); + Assert.Equal(("Simple Result", true), actual); } [Fact] public void Should_return_text_with_variable() { - var result = sut.Get(CultureInfo.CurrentUICulture, "withVar", "fallback", new { var = 5 }); + var actual = sut.Get(CultureInfo.CurrentUICulture, "withVar", "fallback", new { var = 5 }); - Assert.Equal(("Var: 5.", true), result); + Assert.Equal(("Var: 5.", true), actual); } [Fact] public void Should_return_text_with_lower_variable() { - var result = sut.Get(CultureInfo.CurrentUICulture, "withLowerVar", "fallback", new { var = "Lower" }); + var actual = sut.Get(CultureInfo.CurrentUICulture, "withLowerVar", "fallback", new { var = "Lower" }); - Assert.Equal(("Var: lower.", true), result); + Assert.Equal(("Var: lower.", true), actual); } [Fact] public void Should_return_text_with_upper_variable() { - var result = sut.Get(CultureInfo.CurrentUICulture, "withUpperVar", "fallback", new { var = "upper" }); + var actual = sut.Get(CultureInfo.CurrentUICulture, "withUpperVar", "fallback", new { var = "upper" }); - Assert.Equal(("Var: Upper.", true), result); + Assert.Equal(("Var: Upper.", true), actual); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs index ca6a981c8..5068087f3 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs @@ -69,9 +69,9 @@ namespace Squidex.Infrastructure.UsageTracking A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", date, category, ct)) .Returns(counters); - var result = await sut.GetMonthCallsAsync(key, date, category, ct); + var actual = await sut.GetMonthCallsAsync(key, date, category, ct); - Assert.Equal(4, result); + Assert.Equal(4, actual); } [Fact] @@ -85,9 +85,9 @@ namespace Squidex.Infrastructure.UsageTracking A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", date, category, ct)) .Returns(counters); - var result = await sut.GetMonthBytesAsync(key, date, category, ct); + var actual = await sut.GetMonthBytesAsync(key, date, category, ct); - Assert.Equal(14, result); + Assert.Equal(14, actual); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs index 50f0e09cd..a3fbfdb51 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -106,13 +106,13 @@ namespace Squidex.Infrastructure.UsageTracking A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) .Returns(originalData); - var result1 = await sut.GetForMonthAsync(key, date, null, ct); - var result2 = await sut.GetForMonthAsync(key, date, "category2", ct); + var actual1 = await sut.GetForMonthAsync(key, date, null, ct); + var actual2 = await sut.GetForMonthAsync(key, date, "category2", ct); - Assert.Equal(38, result1["A"]); - Assert.Equal(55, result1["B"]); + Assert.Equal(38, actual1["A"]); + Assert.Equal(55, actual1["B"]); - Assert.Equal(22, result2["B"]); + Assert.Equal(22, actual2["B"]); } [Fact] @@ -132,17 +132,17 @@ namespace Squidex.Infrastructure.UsageTracking A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) .Returns(originalData); - var result1 = await sut.GetAsync(key, dateFrom, dateTo, null, ct); - var result2 = await sut.GetAsync(key, dateFrom, dateTo, "category2", ct); + var actual1 = await sut.GetAsync(key, dateFrom, dateTo, null, ct); + var actual2 = await sut.GetAsync(key, dateFrom, dateTo, "category2", ct); - Assert.Equal(38, result1["A"]); - Assert.Equal(55, result1["B"]); + Assert.Equal(38, actual1["A"]); + Assert.Equal(55, actual1["B"]); - Assert.Equal(22, result2["B"]); + Assert.Equal(22, actual2["B"]); } [Fact] - public async Task Should_create_empty_results_with_default_category_is_result_is_empty() + public async Task Should_create_empty_actuals_with_default_category_is_actual_is_empty() { var dateFrom = date; var dateTo = dateFrom.AddDays(4); @@ -150,7 +150,7 @@ namespace Squidex.Infrastructure.UsageTracking A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) .Returns(new List()); - var result = await sut.QueryAsync(key, dateFrom, dateTo, ct); + var actual = await sut.QueryAsync(key, dateFrom, dateTo, ct); var expected = new Dictionary> { @@ -164,11 +164,11 @@ namespace Squidex.Infrastructure.UsageTracking } }; - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] - public async Task Should_create_results_with_filled_days() + public async Task Should_create_actuals_with_filled_days() { var dateFrom = date; var dateTo = dateFrom.AddDays(4); @@ -185,7 +185,7 @@ namespace Squidex.Infrastructure.UsageTracking A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) .Returns(originalData); - var result = await sut.QueryAsync(key, dateFrom, dateTo, ct); + var actual = await sut.QueryAsync(key, dateFrom, dateTo, ct); var expected = new Dictionary> { @@ -207,7 +207,7 @@ namespace Squidex.Infrastructure.UsageTracking } }; - result.Should().BeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } [Fact] @@ -256,19 +256,19 @@ namespace Squidex.Infrastructure.UsageTracking private static Counters Counters(double? a = null, double? b = null) { - var result = new Counters(); + var actual = new Counters(); if (a != null) { - result["A"] = a.Value; + actual["A"] = a.Value; } if (b != null) { - result["B"] = b.Value; + actual["B"] = b.Value; } - return result; + return actual; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs index 8f104e3f4..f8f98f43c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs @@ -88,11 +88,11 @@ namespace Squidex.Infrastructure.UsageTracking A.CallTo(() => inner.GetForMonthAsync(key, date, category, ct)) .Returns(counters); - var result1 = await sut.GetForMonthAsync(key, date, category, ct); - var result2 = await sut.GetForMonthAsync(key, date, category, ct); + var actual1 = await sut.GetForMonthAsync(key, date, category, ct); + var actual2 = await sut.GetForMonthAsync(key, date, category, ct); - Assert.Same(counters, result1); - Assert.Same(counters, result2); + Assert.Same(counters, actual1); + Assert.Same(counters, actual2); A.CallTo(() => inner.GetForMonthAsync(key, DateTime.Today, category, ct)) .MustHaveHappenedOnceExactly(); @@ -109,11 +109,11 @@ namespace Squidex.Infrastructure.UsageTracking A.CallTo(() => inner.GetAsync(key, dateFrom, dateTo, category, ct)) .Returns(counters); - var result1 = await sut.GetAsync(key, dateFrom, dateTo, category, ct); - var result2 = await sut.GetAsync(key, dateFrom, dateTo, category, ct); + var actual1 = await sut.GetAsync(key, dateFrom, dateTo, category, ct); + var actual2 = await sut.GetAsync(key, dateFrom, dateTo, category, ct); - Assert.Same(counters, result1); - Assert.Same(counters, result2); + Assert.Same(counters, actual1); + Assert.Same(counters, actual2); A.CallTo(() => inner.GetAsync(key, dateFrom, dateTo, category, ct)) .MustHaveHappenedOnceExactly(); @@ -125,10 +125,10 @@ namespace Squidex.Infrastructure.UsageTracking var dateFrom = date; var dateTo = dateFrom.AddDays(10); - var result1 = await sut.QueryAsync(key, dateFrom, dateTo, ct); - var result2 = await sut.QueryAsync(key, dateFrom, dateTo, ct); + var actual1 = await sut.QueryAsync(key, dateFrom, dateTo, ct); + var actual2 = await sut.QueryAsync(key, dateFrom, dateTo, ct); - Assert.NotSame(result2, result1); + Assert.NotSame(actual2, actual1); A.CallTo(() => inner.QueryAsync(key, dateFrom, dateTo, ct)) .MustHaveHappenedTwiceOrMore(); diff --git a/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs index dbe11f005..876ddf5f8 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs @@ -54,11 +54,11 @@ namespace Squidex.Infrastructure }; var source = new ValidationException(errors); - var result = source.SerializeAndDeserializeBinary(); + var actual = source.SerializeAndDeserializeBinary(); - result.Errors.Should().BeEquivalentTo(source.Errors); + actual.Errors.Should().BeEquivalentTo(source.Errors); - Assert.Equal(source.Message, result.Message); + Assert.Equal(source.Message, actual.Message); } } } diff --git a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs index 07db8ff2c..4c942c160 100644 --- a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs +++ b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs @@ -45,10 +45,10 @@ namespace Squidex.Web sut.OnException(context); - var result = (ObjectResult)context.Result!; + var actual = (ObjectResult)context.Result!; - Assert.Equal(400, result.StatusCode); - Assert.Equal(400, (result.Value as ErrorDto)?.StatusCode); + Assert.Equal(400, actual.StatusCode); + Assert.Equal(400, (actual.Value as ErrorDto)?.StatusCode); Assert.Equal(new[] { @@ -57,7 +57,7 @@ namespace Squidex.Web "property1, property2: Error3", "property3.property4: Error4", "property5[0].property6: Error5" - }, ((ErrorDto)result.Value!).Details); + }, ((ErrorDto)actual.Value!).Details); A.CallTo(log) .MustNotHaveHappened(); @@ -210,18 +210,18 @@ namespace Squidex.Web { var actionContext = ActionContext(); - var result = context.Result; + var actual = context.Result; - return new ResultExecutedContext(actionContext, new List(), result, context.Controller); + return new ResultExecutedContext(actionContext, new List(), actual, context.Controller); } private ResultExecutingContext Problem(ProblemDetails problem) { var actionContext = ActionContext(); - var result = new ObjectResult(problem) { StatusCode = problem.Status }; + var actual = new ObjectResult(problem) { StatusCode = problem.Status }; - return new ResultExecutingContext(actionContext, new List(), result, null!); + return new ResultExecutingContext(actionContext, new List(), actual, null!); } private ExceptionContext Error(Exception exception) @@ -256,13 +256,13 @@ namespace Squidex.Web private static void Validate(int statusCode, IActionResult? actionResult, Exception? exception, string? errorCode = null) { - var result = actionResult as ObjectResult; + var actual = actionResult as ObjectResult; - var error = result?.Value as ErrorDto; + var error = actual?.Value as ErrorDto; Assert.NotNull(error?.Type); - Assert.Equal(statusCode, result?.StatusCode); + Assert.Equal(statusCode, actual?.StatusCode); Assert.Equal(statusCode, error?.StatusCode); Assert.Equal(errorCode, error?.ErrorCode); diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs index f830bdd72..8c1cb93bb 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs @@ -71,11 +71,11 @@ namespace Squidex.Web.CommandMiddlewares } [Fact] - public async Task Should_add_version_from_result_as_etag_to_response() + public async Task Should_add_version_from_actual_as_etag_to_response() { - var result = CommandResult.Empty(DomainId.Empty, 17, 16); + var actual = CommandResult.Empty(DomainId.Empty, 17, 16); - await HandleAsync(new CreateContent(), result); + await HandleAsync(new CreateContent(), actual); Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext!.Response.Headers[HeaderNames.ETag]); } @@ -83,16 +83,16 @@ namespace Squidex.Web.CommandMiddlewares [Fact] public async Task Should_add_version_from_entity_as_etag_to_response() { - var result = new ContentEntity { Version = 17 }; + var actual = new ContentEntity { Version = 17 }; - await HandleAsync(new CreateContent(), result); + await HandleAsync(new CreateContent(), actual); Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext!.Response.Headers[HeaderNames.ETag]); } - private async Task HandleAsync(ICommand command, object result) + private async Task HandleAsync(ICommand command, object actual) { - var commandContext = new CommandContext(command, A.Fake()).Complete(result); + var commandContext = new CommandContext(command, A.Fake()).Complete(actual); await sut.HandleAsync(commandContext, default); diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs index 25e265401..df1eb8054 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs @@ -42,7 +42,7 @@ namespace Squidex.Web.CommandMiddlewares } [Fact] - public async Task Should_assign_app_id_and_name_to_app_command() + public async Task Should_assign_named_id_to_command() { var context = await HandleAsync(new CreateContent()); @@ -50,7 +50,7 @@ namespace Squidex.Web.CommandMiddlewares } [Fact] - public async Task Should_not_override_app_id_and_name() + public async Task Should_not_override_existing_named_id() { var customId = NamedId.Of(DomainId.NewGuid(), "other-app"); diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs index 5bed9d096..432002a78 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs @@ -9,7 +9,6 @@ using FakeItEasy; using Microsoft.AspNetCore.Http; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -29,6 +28,7 @@ namespace Squidex.Web.CommandMiddlewares public EnrichWithSchemaIdCommandMiddlewareTests() { httpContext.Features.Set(new AppFeature(Mocks.App(appId))); + httpContext.Features.Set(new SchemaFeature(Mocks.Schema(appId, schemaId))); A.CallTo(() => httpContextAccessor.HttpContext) .Returns(httpContext); @@ -39,46 +39,22 @@ namespace Squidex.Web.CommandMiddlewares [Fact] public async Task Should_throw_exception_if_schema_not_found() { + httpContext.Features.Set(null); + await Assert.ThrowsAsync(() => HandleAsync(new CreateContent())); } [Fact] - public async Task Should_assign_schema_id_and_name_to_app_command() + public async Task Should_assign_named_id_to_command() { - httpContext.Features.Set(new SchemaFeature(Mocks.Schema(appId, schemaId))); - var context = await HandleAsync(new CreateContent()); Assert.Equal(schemaId, ((ISchemaCommand)context.Command).SchemaId); } [Fact] - public async Task Should_assign_schema_id_from_id() - { - httpContext.Features.Set(new SchemaFeature(Mocks.Schema(appId, schemaId))); - - var context = await HandleAsync(new UpdateSchema()); - - Assert.Equal(schemaId, ((ISchemaCommand)context.Command).SchemaId); - } - - [Fact] - public async Task Should_not_override_schema_id() + public async Task Should_not_override_existing_named_id() { - httpContext.Features.Set(new SchemaFeature(Mocks.Schema(appId, schemaId))); - - var customId = DomainId.NewGuid(); - - var context = await HandleAsync(new CreateSchema { SchemaId = customId }); - - Assert.Equal(customId, ((CreateSchema)context.Command).SchemaId); - } - - [Fact] - public async Task Should_not_override_schema_id_and_name() - { - httpContext.Features.Set(new SchemaFeature(Mocks.Schema(appId, schemaId))); - var customId = NamedId.Of(DomainId.NewGuid(), "other-app"); var context = await HandleAsync(new CreateContent { SchemaId = customId }); diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithTeamIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithTeamIdCommandMiddlewareTests.cs new file mode 100644 index 000000000..862d9e985 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithTeamIdCommandMiddlewareTests.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Teams.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Web.Pipeline; +using Xunit; + +namespace Squidex.Web.CommandMiddlewares +{ + public class EnrichWithTeamIdCommandMiddlewareTests + { + private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); + private readonly DomainId teamId = DomainId.NewGuid(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly EnrichWithTeamIdCommandMiddleware sut; + + public EnrichWithTeamIdCommandMiddlewareTests() + { + httpContext.Features.Set(new TeamFeature(Mocks.Team(teamId))); + + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + sut = new EnrichWithTeamIdCommandMiddleware(httpContextAccessor); + } + + [Fact] + public async Task Should_throw_exception_if_team_not_found() + { + httpContext.Features.Set(null); + + await Assert.ThrowsAsync(() => HandleAsync(new UpdateTeam())); + } + + [Fact] + public async Task Should_assign_id_to_command() + { + var context = await HandleAsync(new UpdateTeam()); + + Assert.Equal(teamId, ((ITeamCommand)context.Command).TeamId); + } + + [Fact] + public async Task Should_not_override_existing_id() + { + var customId = DomainId.NewGuid(); + + var context = await HandleAsync(new UpdateTeam { TeamId = customId }); + + Assert.Equal(customId, ((ITeamCommand)context.Command).TeamId); + } + + private async Task HandleAsync(ITeamCommand command) + { + var commandContext = new CommandContext(command, A.Fake()); + + await sut.HandleAsync(commandContext, default); + + return commandContext; + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs index 399908bf5..e3ac1f2c4 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs @@ -13,7 +13,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Billing; using Xunit; namespace Squidex.Web.Pipeline @@ -21,7 +21,7 @@ namespace Squidex.Web.Pipeline public class ApiCostsFilterTests { private readonly IAppEntity appEntity = A.Fake(); - private readonly UsageGate usageGate = A.Fake(); + private readonly IAppUsageGate appUsageGate = A.Fake(); private readonly ActionExecutingContext actionContext; private readonly ActionExecutionDelegate next; private readonly HttpContext httpContext = new DefaultHttpContext(); @@ -43,7 +43,7 @@ namespace Squidex.Web.Pipeline return Task.FromResult(null!); }; - sut = new ApiCostsFilter(usageGate); + sut = new ApiCostsFilter(appUsageGate); } [Fact] @@ -53,7 +53,7 @@ namespace Squidex.Web.Pipeline SetupApp(); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) + A.CallTo(() => appUsageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) .Returns(true); await sut.OnActionExecutionAsync(actionContext, next); @@ -69,7 +69,7 @@ namespace Squidex.Web.Pipeline SetupApp(); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) + A.CallTo(() => appUsageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) .Returns(false); await sut.OnActionExecutionAsync(actionContext, next); @@ -88,7 +88,7 @@ namespace Squidex.Web.Pipeline Assert.True(isNextCalled); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) + A.CallTo(() => appUsageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) .MustNotHaveHappened(); } @@ -101,7 +101,7 @@ namespace Squidex.Web.Pipeline Assert.True(isNextCalled); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) + A.CallTo(() => appUsageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs index 7511a5bac..bb14c0336 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs @@ -26,10 +26,10 @@ namespace Squidex.Web.Pipeline userIdentity.AddClaim(new Claim(userIdentity.RoleClaimType, role)); - var result = await sut.TransformAsync(userPrincipal); + var actual = await sut.TransformAsync(userPrincipal); - Assert.Equal(PermissionIds.Admin, result.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.Permissions)?.Value); - Assert.Equal(role, result.Claims.FirstOrDefault(x => x.Type == userIdentity.RoleClaimType)?.Value); + Assert.Equal(PermissionIds.Admin, actual.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.Permissions)?.Value); + Assert.Equal(role, actual.Claims.FirstOrDefault(x => x.Type == userIdentity.RoleClaimType)?.Value); } [Fact] @@ -40,9 +40,9 @@ namespace Squidex.Web.Pipeline userIdentity.AddClaim(new Claim(userIdentity.RoleClaimType, "Developer")); - var result = await sut.TransformAsync(userPrincipal); + var actual = await sut.TransformAsync(userPrincipal); - Assert.Single(result.Claims); + Assert.Single(actual.Claims); } [Fact] @@ -51,9 +51,9 @@ namespace Squidex.Web.Pipeline var userIdentity = new ClaimsIdentity(); var userPrincipal = new ClaimsPrincipal(userIdentity); - var result = await sut.TransformAsync(userPrincipal); + var actual = await sut.TransformAsync(userPrincipal); - Assert.Empty(result.Claims); + Assert.Empty(actual.Claims); } } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs index 38667f73d..ffb62dfb8 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; @@ -61,7 +62,7 @@ namespace Squidex.Web.Pipeline [InlineData(null)] [InlineData("")] [InlineData(" ")] - public async Task Should_return_not_found_if_app_name_is_null(string? app) + public async Task Should_return_404_if_app_name_is_null(string? app) { SetupUser(); @@ -77,7 +78,7 @@ namespace Squidex.Web.Pipeline } [Fact] - public async Task Should_return_not_found_if_app_not_found() + public async Task Should_return_404_if_app_not_found() { SetupUser(); @@ -114,7 +115,7 @@ namespace Squidex.Web.Pipeline var app = CreateApp(appName); user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, $"squidex.apps.{appName}")); A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) .Returns(app); @@ -126,7 +127,7 @@ namespace Squidex.Web.Pipeline Assert.Same(app, httpContext.Context().App); Assert.True(user.Claims.Any()); Assert.True(permissions.Count < 3); - Assert.True(permissions.All(x => x.Value.StartsWith("squidex.apps.my-app", StringComparison.OrdinalIgnoreCase))); + Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.apps.{appName}", StringComparison.OrdinalIgnoreCase))); Assert.True(isNextCalled); } @@ -135,7 +136,7 @@ namespace Squidex.Web.Pipeline { var user = SetupUser(); - var app = CreateApp(appName, appUser: "user1"); + var app = CreateApp(appName, user: "user1"); user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); @@ -149,7 +150,7 @@ namespace Squidex.Web.Pipeline Assert.Same(app, httpContext.Context().App); Assert.True(user.Claims.Count() > 2); Assert.True(permissions.Count < 3); - Assert.True(permissions.All(x => x.Value.StartsWith("squidex.apps.my-app", StringComparison.OrdinalIgnoreCase))); + Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.apps.{appName}", StringComparison.OrdinalIgnoreCase))); Assert.True(isNextCalled); } @@ -158,7 +159,7 @@ namespace Squidex.Web.Pipeline { var user = SetupUser(); - var app = CreateApp(appName, appUser: "user1"); + var app = CreateApp(appName, user: "user1"); user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); user.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); @@ -173,7 +174,7 @@ namespace Squidex.Web.Pipeline Assert.Same(app, httpContext.Context().App); Assert.True(user.Claims.Count() > 2); Assert.True(permissions.Count > 10); - Assert.True(permissions.All(x => x.Value.StartsWith("squidex.apps.my-app", StringComparison.OrdinalIgnoreCase))); + Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.apps.{appName}", StringComparison.OrdinalIgnoreCase))); Assert.True(isNextCalled); } @@ -184,7 +185,7 @@ namespace Squidex.Web.Pipeline user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); - var app = CreateApp(appName, appClient: "client1"); + var app = CreateApp(appName, client: "client1"); A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) .Returns(app); @@ -201,7 +202,7 @@ namespace Squidex.Web.Pipeline { var user = SetupUser(); - var app = CreateApp(appName, appClient: "client1", allowAnonymous: true); + var app = CreateApp(appName, client: "client1", allowAnonymous: true); A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) .Returns(app); @@ -237,7 +238,7 @@ namespace Squidex.Web.Pipeline } [Fact] - public async Task Should_return_not_found_if_user_has_no_permissions() + public async Task Should_return_404_if_user_has_no_permissions() { var user = SetupUser(); @@ -256,13 +257,13 @@ namespace Squidex.Web.Pipeline } [Fact] - public async Task Should_return_not_found_if_client_is_from_another_app() + public async Task Should_return_404_if_client_is_from_another_app() { var user = SetupUser(); user.AddClaim(new Claim(OpenIdClaims.ClientId, "other:client1")); - var app = CreateApp(appName, appClient: "client1"); + var app = CreateApp(appName, client: "client1"); A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) .Returns(app); @@ -296,22 +297,22 @@ namespace Squidex.Web.Pipeline return userIdentity; } - private static IAppEntity CreateApp(string name, string? appUser = null, string? appClient = null, long? apiCallsLimit = null, bool? allowAnonymous = null) + private static IAppEntity CreateApp(string name, string? user = null, string? client = null, bool? allowAnonymous = null) { var app = A.Fake(); - var contributors = AppContributors.Empty; + var contributors = Contributors.Empty; - if (appUser != null) + if (user != null) { - contributors = contributors.Assign(appUser, Role.Reader); + contributors = contributors.Assign(user, Role.Reader); } var clients = AppClients.Empty; - if (appClient != null) + if (client != null) { - clients = clients.Add(appClient, "secret").Update(appClient, apiCallsLimit: apiCallsLimit, allowAnonymous: allowAnonymous); + clients = clients.Add(client, "secret").Update(client, allowAnonymous: allowAnonymous); } A.CallTo(() => app.Contributors).Returns(contributors); diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs index 9a8f8241d..d0dcb3c18 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs @@ -18,7 +18,7 @@ namespace Squidex.Web.Pipeline public class RequestExceptionMiddlewareTests { private readonly ILogger log = A.Fake>(); - private readonly IActionResultExecutor resultWriter = A.Fake>(); + private readonly IActionResultExecutor actualWriter = A.Fake>(); private readonly IHttpResponseFeature responseFeature = A.Fake(); private readonly HttpContext httpContext = new DefaultHttpContext(); private readonly RequestDelegate next; @@ -43,11 +43,11 @@ namespace Squidex.Web.Pipeline var sut = new RequestExceptionMiddleware(next); - await sut.InvokeAsync(httpContext, resultWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); Assert.False(isNextCalled); - A.CallTo(() => resultWriter.ExecuteAsync(A._, + A.CallTo(() => actualWriter.ExecuteAsync(A._, A.That.Matches(x => x.StatusCode == 412 && x.Value is ErrorDto))) .MustHaveHappened(); } @@ -59,11 +59,11 @@ namespace Squidex.Web.Pipeline var sut = new RequestExceptionMiddleware(next); - await sut.InvokeAsync(httpContext, resultWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); Assert.True(isNextCalled); - A.CallTo(() => resultWriter.ExecuteAsync(A._, A._)) + A.CallTo(() => actualWriter.ExecuteAsync(A._, A._)) .MustNotHaveHappened(); } @@ -74,11 +74,11 @@ namespace Squidex.Web.Pipeline var sut = new RequestExceptionMiddleware(next); - await sut.InvokeAsync(httpContext, resultWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); Assert.True(isNextCalled); - A.CallTo(() => resultWriter.ExecuteAsync(A._, A._)) + A.CallTo(() => actualWriter.ExecuteAsync(A._, A._)) .MustNotHaveHappened(); } @@ -92,9 +92,9 @@ namespace Squidex.Web.Pipeline var sut = new RequestExceptionMiddleware(failingNext); - await sut.InvokeAsync(httpContext, resultWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); - A.CallTo(() => resultWriter.ExecuteAsync(A._, + A.CallTo(() => actualWriter.ExecuteAsync(A._, A.That.Matches(x => x.StatusCode == 500 && x.Value is ErrorDto))) .MustHaveHappened(); } @@ -111,7 +111,7 @@ namespace Squidex.Web.Pipeline var sut = new RequestExceptionMiddleware(failingNext); - await sut.InvokeAsync(httpContext, resultWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument(0) == LogLevel.Error) .MustHaveHappened(); @@ -130,9 +130,9 @@ namespace Squidex.Web.Pipeline var sut = new RequestExceptionMiddleware(failingNext); - await sut.InvokeAsync(httpContext, resultWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); - A.CallTo(() => resultWriter.ExecuteAsync(A._, A._)) + A.CallTo(() => actualWriter.ExecuteAsync(A._, A._)) .MustNotHaveHappened(); } @@ -148,9 +148,9 @@ namespace Squidex.Web.Pipeline var sut = new RequestExceptionMiddleware(failingNext); - await sut.InvokeAsync(httpContext, resultWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); - A.CallTo(() => resultWriter.ExecuteAsync(A._, + A.CallTo(() => actualWriter.ExecuteAsync(A._, A.That.Matches(x => x.StatusCode == 412 && x.Value is ErrorDto))) .MustHaveHappened(); } @@ -170,9 +170,9 @@ namespace Squidex.Web.Pipeline var sut = new RequestExceptionMiddleware(failingNext); - await sut.InvokeAsync(httpContext, resultWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); - A.CallTo(() => resultWriter.ExecuteAsync(A._, A._)) + A.CallTo(() => actualWriter.ExecuteAsync(A._, A._)) .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs index 3a4621be3..14f4bfbcc 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs @@ -64,7 +64,7 @@ namespace Squidex.Web.Pipeline [InlineData(null)] [InlineData("")] [InlineData(" ")] - public async Task Should_return_not_found_if_schema_name_is_null(string? schema) + public async Task Should_return_404_if_schema_name_is_null(string? schema) { actionContext.RouteData.Values["schema"] = schema; @@ -77,7 +77,7 @@ namespace Squidex.Web.Pipeline } [Fact] - public async Task Should_return_not_found_if_schema_not_published_when_attribute_applied() + public async Task Should_return_404_if_schema_not_published_when_attribute_applied() { actionContext.ActionDescriptor.EndpointMetadata.Add(new SchemaMustBePublishedAttribute()); actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); @@ -108,7 +108,7 @@ namespace Squidex.Web.Pipeline } [Fact] - public async Task Should_return_not_found_if_schema_not_found() + public async Task Should_return_404_if_schema_not_found() { actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/TeamResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/TeamResolverTests.cs new file mode 100644 index 000000000..636b58208 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Pipeline/TeamResolverTests.cs @@ -0,0 +1,238 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Claims; +using FakeItEasy; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Teams; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; +using Xunit; + +#pragma warning disable IDE0017 // Simplify object initialization + +namespace Squidex.Web.Pipeline +{ + public class TeamResolverTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionContext actionContext; + private readonly ActionExecutingContext actionExecutingContext; + private readonly ActionExecutionDelegate next; + private readonly DomainId teamId = DomainId.NewGuid(); + private readonly TeamResolver sut; + private bool isNextCalled; + + public TeamResolverTests() + { + actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor + { + EndpointMetadata = new List() + }); + + actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); + actionExecutingContext.HttpContext = httpContext; + actionExecutingContext.RouteData.Values["team"] = teamId.ToString(); + + next = () => + { + isNextCalled = true; + + return Task.FromResult(null!); + }; + + sut = new TeamResolver(appProvider); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task Should_return_404_if_team_name_is_null(string? team) + { + SetupUser(); + + actionExecutingContext.RouteData.Values["team"] = team; + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + + A.CallTo(() => appProvider.GetTeamAsync(A._, httpContext.RequestAborted)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_return_404_if_team_not_found() + { + SetupUser(); + + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(Task.FromResult(null)); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_return_401_if_user_is_anonymous() + { + SetupUser(null); + + var team = CreateTeam(teamId); + + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(team); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_resolve_team_from_user() + { + var user = SetupUser(); + + var team = CreateTeam(teamId); + + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, $"squidex.teams.{teamId}")); + + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(team); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); + + Assert.Same(team, httpContext.Features.Get()!.Team); + Assert.True(user.Claims.Any()); + Assert.True(permissions.Count < 3); + Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.teams.{teamId}", StringComparison.OrdinalIgnoreCase))); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_resolve_team_from_contributor() + { + var user = SetupUser(); + + var team = CreateTeam(teamId, user: "user1"); + + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(team); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); + + Assert.Same(team, httpContext.Features.Get()!.Team); + Assert.True(user.Claims.Count() > 2); + Assert.True(permissions.Count < 3); + Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.teams.{teamId}", StringComparison.OrdinalIgnoreCase))); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_resolve_team_if_action_allows_anonymous_but_user_has_no_permissions() + { + var user = SetupUser(); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{teamId}:client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.teams.other-team")); + + var team = CreateTeam(teamId); + + actionContext.ActionDescriptor.EndpointMetadata.Add(new AllowAnonymousAttribute()); + + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(team); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(team, httpContext.Features.Get()!.Team); + Assert.Equal(2, user.Claims.Count()); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_return_404_if_user_has_no_permissions() + { + var user = SetupUser(); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{teamId}:client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.teams.other-team")); + + var team = CreateTeam(teamId); + + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(team); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_do_nothing_if_parameter_not_set() + { + actionExecutingContext.RouteData.Values.Remove("team"); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => appProvider.GetTeamAsync(A._, httpContext.RequestAborted)) + .MustNotHaveHappened(); + } + + private ClaimsIdentity SetupUser(string? type = "OIDC") + { + var userIdentity = new ClaimsIdentity(type); + var userPrincipal = new ClaimsPrincipal(userIdentity); + + actionExecutingContext.HttpContext.User = userPrincipal; + + return userIdentity; + } + + private static ITeamEntity CreateTeam(DomainId id, string? user = null) + { + var team = A.Fake(); + + var contributors = Contributors.Empty; + + if (user != null) + { + contributors = contributors.Assign(user, Role.Owner); + } + + A.CallTo(() => team.Id).Returns(id); + A.CallTo(() => team.Contributors).Returns(contributors); + + return team; + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs index 125de77ee..f258ac734 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs @@ -10,9 +10,9 @@ using FakeItEasy; using Microsoft.AspNetCore.Http; using NodaTime; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Billing; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; -using Squidex.Infrastructure.UsageTracking; using Xunit; namespace Squidex.Web.Pipeline @@ -20,7 +20,7 @@ namespace Squidex.Web.Pipeline public class UsageMiddlewareTests { private readonly IAppLogStore appLogStore = A.Fake(); - private readonly IApiUsageTracker usageTracker = A.Fake(); + private readonly IAppUsageGate appUsageGate = A.Fake(); private readonly IClock clock = A.Fake(); private readonly Instant instant = SystemClock.Instance.GetCurrentInstant(); private readonly HttpContext httpContext = new DefaultHttpContext(); @@ -41,7 +41,7 @@ namespace Squidex.Web.Pipeline return Task.CompletedTask; }; - sut = new UsageMiddleware(appLogStore, usageTracker) + sut = new UsageMiddleware(appLogStore, appUsageGate) { Clock = clock }; @@ -56,14 +56,16 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, A._, A._, A._, default)) + A.CallTo(() => appUsageGate.TrackRequestAsync(A._, A._, A._, A._, A._, A._, default)) .MustNotHaveHappened(); } [Fact] public async Task Should_not_track_if_call_blocked() { - httpContext.Features.Set(new AppFeature(Mocks.App(appId))); + var app = Mocks.App(appId); + + httpContext.Features.Set(new AppFeature(app)); httpContext.Features.Set(new ApiCostsAttribute(13)); httpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; @@ -74,14 +76,16 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, A._, A._, A._, default)) + A.CallTo(() => appUsageGate.TrackRequestAsync(app, A._, date, A._, A._, A._, default)) .MustNotHaveHappened(); } [Fact] public async Task Should_track_if_calls_left() { - httpContext.Features.Set(new AppFeature(Mocks.App(appId))); + var app = Mocks.App(appId); + + httpContext.Features.Set(new AppFeature(app)); httpContext.Features.Set(new ApiCostsAttribute(13)); await sut.InvokeAsync(httpContext, next); @@ -90,14 +94,16 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, A._, default)) + A.CallTo(() => appUsageGate.TrackRequestAsync(app, A._, date, 13, A._, A._, default)) .MustHaveHappened(); } [Fact] public async Task Should_track_request_bytes() { - httpContext.Features.Set(new AppFeature(Mocks.App(appId))); + var app = Mocks.App(appId); + + httpContext.Features.Set(new AppFeature(app)); httpContext.Features.Set(new ApiCostsAttribute(13)); httpContext.Request.ContentLength = 1024; @@ -107,14 +113,16 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 1024, default)) + A.CallTo(() => appUsageGate.TrackRequestAsync(app, A._, date, 13, A._, 1024, default)) .MustHaveHappened(); } [Fact] public async Task Should_track_response_bytes_with_writer() { - httpContext.Features.Set(new AppFeature(Mocks.App(appId))); + var app = Mocks.App(appId); + + httpContext.Features.Set(new AppFeature(app)); httpContext.Features.Set(new ApiCostsAttribute(13)); await sut.InvokeAsync(httpContext, async x => @@ -128,14 +136,16 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 11, default)) + A.CallTo(() => appUsageGate.TrackRequestAsync(app, A._, date, 13, A._, 11, default)) .MustHaveHappened(); } [Fact] public async Task Should_track_response_bytes_with_stream() { - httpContext.Features.Set(new AppFeature(Mocks.App(appId))); + var app = Mocks.App(appId); + + httpContext.Features.Set(new AppFeature(app)); httpContext.Features.Set(new ApiCostsAttribute(13)); await sut.InvokeAsync(httpContext, async x => @@ -149,14 +159,16 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 11, default)) + A.CallTo(() => appUsageGate.TrackRequestAsync(app, A._, date, 13, A._, 11, default)) .MustHaveHappened(); } [Fact] public async Task Should_track_response_bytes_with_file() { - httpContext.Features.Set(new AppFeature(Mocks.App(appId))); + var app = Mocks.App(appId); + + httpContext.Features.Set(new AppFeature(app)); httpContext.Features.Set(new ApiCostsAttribute(13)); var tempFileName = Path.GetTempFileName(); @@ -180,14 +192,16 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 11, default)) + A.CallTo(() => appUsageGate.TrackRequestAsync(app, A._, date, 13, A._, 11, default)) .MustHaveHappened(); } [Fact] public async Task Should_not_track_if_costs_are_zero() { - httpContext.Features.Set(new AppFeature(Mocks.App(appId))); + var app = Mocks.App(appId); + + httpContext.Features.Set(new AppFeature(app)); httpContext.Features.Set(new ApiCostsAttribute(0)); await sut.InvokeAsync(httpContext, next); @@ -196,14 +210,16 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, A._, A._, A._, default)) + A.CallTo(() => appUsageGate.TrackRequestAsync(app, A._, date, A._, A._, A._, default)) .MustNotHaveHappened(); } [Fact] public async Task Should_log_request_even_if_costs_are_zero() { - httpContext.Features.Set(new AppFeature(Mocks.App(appId))); + var app = Mocks.App(appId); + + httpContext.Features.Set(new AppFeature(app)); httpContext.Features.Set(new ApiCostsAttribute(0)); httpContext.Request.Method = "GET"; diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.Should_create_app_with_anonymous_read_access.verified.txt b/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.Should_create_app_with_anonymous_read_access.verified.txt index 3369044ef..6b0b430ef 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.Should_create_app_with_anonymous_read_access.verified.txt +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.Should_create_app_with_anonymous_read_access.verified.txt @@ -55,6 +55,9 @@ settings: { method: GET }, + transfer: { + method: PUT + }, update: { method: PUT }, diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.Should_create_app_with_anonymous_write_access.verified.txt b/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.Should_create_app_with_anonymous_write_access.verified.txt index 3369044ef..6b0b430ef 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.Should_create_app_with_anonymous_write_access.verified.txt +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.Should_create_app_with_anonymous_write_access.verified.txt @@ -55,6 +55,9 @@ settings: { method: GET }, + transfer: { + method: PUT + }, update: { method: PUT }, diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.Should_create_app.verified.txt b/backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.Should_create_app.verified.txt index 3369044ef..6b0b430ef 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.Should_create_app.verified.txt +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.Should_create_app.verified.txt @@ -55,6 +55,9 @@ settings: { method: GET }, + transfer: { + method: PUT + }, update: { method: PUT }, diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index df49c6319..094c5ef0d 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -7,8 +7,8 @@ import { ModuleWithProviders } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { AppMustExistGuard, LoadAppsGuard, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, UnsetAppGuard } from './shared'; -import { AppAreaComponent, ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LoginPageComponent, LogoutPageComponent, NotFoundPageComponent } from './shell'; +import { AppMustExistGuard, LoadAppsGuard, LoadTeamsGuard, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, TeamMustExistGuard, UnsetAppGuard, UnsetTeamGuard } from './shared'; +import { AppAreaComponent, ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LoginPageComponent, LogoutPageComponent, NotFoundPageComponent, TeamsAreaComponent } from './shell'; const routes: Routes = [ { @@ -19,17 +19,29 @@ const routes: Routes = [ { path: 'app', component: InternalAreaComponent, - canActivate: [MustBeAuthenticatedGuard, LoadAppsGuard], + canActivate: [MustBeAuthenticatedGuard, LoadAppsGuard, LoadTeamsGuard], children: [ { path: '', loadChildren: () => import('./features/apps/module').then(m => m.SqxFeatureAppsModule), - canActivate: [UnsetAppGuard], + canActivate: [UnsetAppGuard, UnsetTeamGuard], }, { path: 'administration', loadChildren: () => import('./features/administration/module').then(m => m.SqxFeatureAdministrationModule), - canActivate: [UnsetAppGuard], + canActivate: [UnsetAppGuard, UnsetTeamGuard], + }, + { + path: 'teams', + component: TeamsAreaComponent, + canActivate: [UnsetAppGuard, UnsetTeamGuard], + children: [ + { + path: ':teamName', + canActivate: [TeamMustExistGuard], + loadChildren: () => import('./features/teams/module').then(m => m.SqxFeatureTeamsModule), + }, + ], }, { path: ':appName', diff --git a/frontend/src/app/features/administration/state/event-consumers.state.ts b/frontend/src/app/features/administration/state/event-consumers.state.ts index 30285ff22..5a6118628 100644 --- a/frontend/src/app/features/administration/state/event-consumers.state.ts +++ b/frontend/src/app/features/administration/state/event-consumers.state.ts @@ -8,22 +8,14 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { DialogService, shareSubscribed, State } from '@app/shared'; +import { DialogService, LoadingState, shareSubscribed, State } from '@app/shared'; import { EventConsumerDto, EventConsumersService } from './../services/event-consumers.service'; -interface Snapshot { +interface Snapshot extends LoadingState { // The list of event consumers. - eventConsumers: EventConsumersList; - - // Indicates if event consumers are loaded. - isLoaded?: boolean; - - // Indicates if event consumers are loading. - isLoading?: boolean; + eventConsumers: ReadonlyArray; } -type EventConsumersList = ReadonlyArray; - @Injectable() export class EventConsumersState extends State { public eventConsumers = diff --git a/frontend/src/app/features/administration/state/users.state.ts b/frontend/src/app/features/administration/state/users.state.ts index 99bd830e2..37a689dbe 100644 --- a/frontend/src/app/features/administration/state/users.state.ts +++ b/frontend/src/app/features/administration/state/users.state.ts @@ -25,9 +25,6 @@ interface Snapshot extends ListState { canCreate?: boolean; } -export type UsersList = ReadonlyArray; -export type UsersResult = { total: number; users: UsersList }; - @Injectable() export class UsersState extends State { public users = diff --git a/frontend/src/app/features/apps/declarations.ts b/frontend/src/app/features/apps/declarations.ts index 9a4a23ce2..e2a6af063 100644 --- a/frontend/src/app/features/apps/declarations.ts +++ b/frontend/src/app/features/apps/declarations.ts @@ -9,3 +9,4 @@ export * from './pages/app.component'; export * from './pages/apps-page.component'; export * from './pages/news-dialog.component'; export * from './pages/onboarding-dialog.component'; +export * from './pages/team.component'; \ No newline at end of file diff --git a/frontend/src/app/features/apps/module.ts b/frontend/src/app/features/apps/module.ts index 952dd53ea..12a234158 100644 --- a/frontend/src/app/features/apps/module.ts +++ b/frontend/src/app/features/apps/module.ts @@ -8,7 +8,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { SqxFrameworkModule, SqxSharedModule } from '@app/shared'; -import { AppComponent, AppsPageComponent, NewsDialogComponent, OnboardingDialogComponent } from './declarations'; +import { AppComponent, AppsPageComponent, NewsDialogComponent, OnboardingDialogComponent, TeamComponent } from './declarations'; const routes: Routes = [ { @@ -28,6 +28,7 @@ const routes: Routes = [ AppsPageComponent, NewsDialogComponent, OnboardingDialogComponent, + TeamComponent, ], }) export class SqxFeatureAppsModule {} diff --git a/frontend/src/app/features/apps/pages/apps-page.component.html b/frontend/src/app/features/apps/pages/apps-page.component.html index a87040f80..38950c451 100644 --- a/frontend/src/app/features/apps/pages/apps-page.component.html +++ b/frontend/src/app/features/apps/pages/apps-page.component.html @@ -14,13 +14,25 @@

{{ 'apps.empty' | sqxTranslate }}

+ +

{{ 'common.apps' | sqxTranslate }}

+ [app]="app" (leave)="leaveApp($event)"> + +
+

{{ 'common.teams' | sqxTranslate }}

+ + + +
+
+
diff --git a/frontend/src/app/features/apps/pages/apps-page.component.scss b/frontend/src/app/features/apps/pages/apps-page.component.scss index bb819659c..d94517a73 100644 --- a/frontend/src/app/features/apps/pages/apps-page.component.scss +++ b/frontend/src/app/features/apps/pages/apps-page.component.scss @@ -1,11 +1,14 @@ @import 'mixins'; @import 'vars'; -.apps { - &-section { - @include clearfix; - padding: 2rem 1.25rem 0 $size-sidebar-width + .25rem; - } +.apps-section { + @include clearfix; + padding: 2rem 1.25rem 0 $size-sidebar-width + .25rem; +} + +.teams-section { + @include clearfix; + padding: 2rem 1.25rem 0 $size-sidebar-width + .25rem; } .page { diff --git a/frontend/src/app/features/apps/pages/apps-page.component.ts b/frontend/src/app/features/apps/pages/apps-page.component.ts index fa1102d4c..0b81ebb42 100644 --- a/frontend/src/app/features/apps/pages/apps-page.component.ts +++ b/frontend/src/app/features/apps/pages/apps-page.component.ts @@ -8,7 +8,7 @@ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; -import { AppDto, AppsState, AuthService, DialogModel, FeatureDto, LocalStoreService, NewsService, OnboardingService, TemplateDto, TemplatesState, UIOptions, UIState } from '@app/shared'; +import { AppDto, AppsState, AuthService, DialogModel, FeatureDto, LocalStoreService, NewsService, OnboardingService, TeamDto, TeamsState, TemplateDto, TemplatesState, UIOptions, UIState } from '@app/shared'; import { Settings } from '@app/shared/state/settings'; @Component({ @@ -35,6 +35,7 @@ export class AppsPageComponent implements OnInit { public readonly appsState: AppsState, public readonly authState: AuthService, public readonly uiState: UIState, + public readonly teamsState: TeamsState, private readonly localStore: LocalStoreService, private readonly newsService: NewsService, private readonly onboardingService: OnboardingService, @@ -79,11 +80,19 @@ export class AppsPageComponent implements OnInit { this.addAppDialog.show(); } - public leave(app: AppDto) { + public leaveApp(app: AppDto) { this.appsState.leave(app); } + public leaveTeam(team: TeamDto) { + this.teamsState.leave(team); + } + public trackByApp(_index: number, app: AppDto) { return app.id; } + + public trackByTeam(_index: number, team: TeamDto) { + return team.id; + } } diff --git a/frontend/src/app/features/apps/pages/team.component.html b/frontend/src/app/features/apps/pages/team.component.html new file mode 100644 index 000000000..7a5f5a8ad --- /dev/null +++ b/frontend/src/app/features/apps/pages/team.component.html @@ -0,0 +1,29 @@ +
+
+
+
+

{{team.name}}

+ + +
+
+ + + + + + + {{ 'teams.leave' | sqxTranslate }} + + + +
+
\ No newline at end of file diff --git a/frontend/src/app/features/apps/pages/team.component.scss b/frontend/src/app/features/apps/pages/team.component.scss new file mode 100644 index 000000000..9a9e5321d --- /dev/null +++ b/frontend/src/app/features/apps/pages/team.component.scss @@ -0,0 +1,14 @@ +@import 'mixins'; +@import 'vars'; + +.btn { + @include absolute(1rem, 1rem); +} + +.card-body { + position: relative; +} + +.card-title { + padding-right: 2rem; +} \ No newline at end of file diff --git a/frontend/src/app/features/apps/pages/team.component.ts b/frontend/src/app/features/apps/pages/team.component.ts new file mode 100644 index 000000000..b0a280571 --- /dev/null +++ b/frontend/src/app/features/apps/pages/team.component.ts @@ -0,0 +1,25 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { ModalModel, TeamDto } from '@app/shared'; + +@Component({ + selector: 'sqx-team[team]', + styleUrls: ['./team.component.scss'], + templateUrl: './team.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TeamComponent { + @Input() + public team!: TeamDto; + + @Output() + public leave = new EventEmitter(); + + public dropdown = new ModalModel(); +} diff --git a/frontend/src/app/features/dashboard/declarations.ts b/frontend/src/app/features/dashboard/declarations.ts index 6cfce32ee..227fec6c3 100644 --- a/frontend/src/app/features/dashboard/declarations.ts +++ b/frontend/src/app/features/dashboard/declarations.ts @@ -5,22 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -export * from './pages/cards/api-calls-card.component'; -export * from './pages/cards/api-calls-summary-card.component'; + export * from './pages/cards/api-card.component'; -export * from './pages/cards/api-performance-card.component'; -export * from './pages/cards/api-traffic-card.component'; -export * from './pages/cards/api-traffic-summary-card.component'; -export * from './pages/cards/asset-uploads-count-card.component'; -export * from './pages/cards/asset-uploads-size-card.component'; -export * from './pages/cards/asset-uploads-size-summary-card.component'; export * from './pages/cards/content-summary-card.component'; export * from './pages/cards/github-card.component'; export * from './pages/cards/history-card.component'; -export * from './pages/cards/iframe-card.component'; -export * from './pages/cards/random-cat-card.component'; -export * from './pages/cards/random-dog-card.component'; export * from './pages/cards/schema-card.component'; -export * from './pages/cards/support-card.component'; export * from './pages/dashboard-config.component'; export * from './pages/dashboard-page.component'; diff --git a/frontend/src/app/features/dashboard/module.ts b/frontend/src/app/features/dashboard/module.ts index 8b9f6f483..d4b9ae12f 100644 --- a/frontend/src/app/features/dashboard/module.ts +++ b/frontend/src/app/features/dashboard/module.ts @@ -10,7 +10,7 @@ import { RouterModule, Routes } from '@angular/router'; import { GridsterModule } from 'angular-gridster2'; import { ChartModule } from 'angular2-chartjs'; import { SqxFrameworkModule, SqxSharedModule } from '@app/shared'; -import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, ContentSummaryCardComponent, DashboardConfigComponent, DashboardPageComponent, GithubCardComponent, HistoryCardComponent, IFrameCardComponent, RandomCatCardComponent, RandomDogCardComponent, SchemaCardComponent, SupportCardComponent } from './declarations'; +import { ApiCardComponent, ContentSummaryCardComponent, DashboardConfigComponent, DashboardPageComponent, GithubCardComponent, HistoryCardComponent, SchemaCardComponent } from './declarations'; const routes: Routes = [ { @@ -28,25 +28,13 @@ const routes: Routes = [ SqxSharedModule, ], declarations: [ - ApiCallsCardComponent, - ApiCallsSummaryCardComponent, ApiCardComponent, - ApiPerformanceCardComponent, - ApiTrafficCardComponent, - ApiTrafficSummaryCardComponent, - AssetUploadsCountCardComponent, - AssetUploadsSizeCardComponent, - AssetUploadsSizeSummaryCardComponent, ContentSummaryCardComponent, DashboardConfigComponent, DashboardPageComponent, GithubCardComponent, HistoryCardComponent, - IFrameCardComponent, - RandomCatCardComponent, - RandomDogCardComponent, SchemaCardComponent, - SupportCardComponent, ], }) export class SqxFeatureDashboardModule {} diff --git a/frontend/src/app/features/dashboard/pages/cards/github-card.component.ts b/frontend/src/app/features/dashboard/pages/cards/github-card.component.ts index fafe57553..13c868953 100644 --- a/frontend/src/app/features/dashboard/pages/cards/github-card.component.ts +++ b/frontend/src/app/features/dashboard/pages/cards/github-card.component.ts @@ -5,16 +5,13 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { AppDto } from '@app/shared'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; @Component({ - selector: 'sqx-github-card[app]', + selector: 'sqx-github-card', styleUrls: ['./github-card.component.scss'], templateUrl: './github-card.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class GithubCardComponent { - @Input() - public app!: AppDto; } diff --git a/frontend/src/app/features/dashboard/pages/dashboard-config.component.html b/frontend/src/app/features/dashboard/pages/dashboard-config.component.html index ed9599393..9c2d57f95 100644 --- a/frontend/src/app/features/dashboard/pages/dashboard-config.component.html +++ b/frontend/src/app/features/dashboard/pages/dashboard-config.component.html @@ -17,9 +17,13 @@ - {{ 'common.expertMode' | sqxTranslate }} + + {{ 'common.expertMode' | sqxTranslate }} + - {{ 'common.save' | sqxTranslate }} + + {{ 'common.save' | sqxTranslate }} + diff --git a/frontend/src/app/features/dashboard/pages/dashboard-config.component.ts b/frontend/src/app/features/dashboard/pages/dashboard-config.component.ts index 05ff790c8..4482f0c40 100644 --- a/frontend/src/app/features/dashboard/pages/dashboard-config.component.ts +++ b/frontend/src/app/features/dashboard/pages/dashboard-config.component.ts @@ -22,13 +22,19 @@ export class DashboardConfigComponent implements OnChanges { @Input() public config!: GridsterItem[]; + @Input() + public configDefaults!: GridsterItem[]; + + @Input() + public configAvailable!: GridsterItem[]; + @Input() public needsAttention?: boolean | null; @Output() public configChange = new EventEmitter(); - public configOptions: ReadonlyArray; + public configOptions: ReadonlyArray = []; public expertDialog = new DialogModel(); public expertConfig?: GridsterItem[]; @@ -38,21 +44,23 @@ export class DashboardConfigComponent implements OnChanges { constructor( public readonly appsState: AppsState, public readonly authState: AuthService, + private readonly localizer: LocalizerService, private readonly dialogs: DialogService, private readonly uiState: UIState, - localizer: LocalizerService, ) { - this.configOptions = - [...OPTIONAL_CARDS, ...DEFAULT_CONFIG].map(item => { - const name = localizer.getOrKey(item.name); + } + + public ngOnChanges(changes: SimpleChanges) { + if (changes['configAvailable']) { + this.configOptions = this.configAvailable.map(item => { + const name = this.localizer.getOrKey(item.name); return { ...item, name }; }).sortByString(x => x.name); - } + } - public ngOnChanges(changes: SimpleChanges) { if (changes['app']) { - this.uiState.getUser('dashboard.grid', DEFAULT_CONFIG).pipe(take(1)) + this.uiState.getUser('dashboard.grid', this.configDefaults).pipe(take(1)) .subscribe(dto => { this.setConfig(dto); }); @@ -61,7 +69,7 @@ export class DashboardConfigComponent implements OnChanges { private setConfig(config: any) { if (!Types.isArrayOfObject(config)) { - config = DEFAULT_CONFIG; + config = this.configDefaults; } this.configChange.emit(Types.clone(config)); @@ -82,7 +90,7 @@ export class DashboardConfigComponent implements OnChanges { } public resetConfig() { - this.setConfig(Types.clone(DEFAULT_CONFIG)); + this.setConfig(Types.clone(this.configDefaults)); this.saveConfig(); } @@ -117,34 +125,3 @@ export class DashboardConfigComponent implements OnChanges { return this.config.find(x => x.type === item.type); } } - -const DEFAULT_CONFIG: GridsterItem[] = [ - // Row 1 - { cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.schemasCard', type: 'schemas' }, - { cols: 1, rows: 1, x: 1, y: 0, name: 'i18n:dashboard.apiDocumentationCard', type: 'api' }, - { cols: 1, rows: 1, x: 2, y: 0, name: 'i18n:dashboard.supportCard', type: 'support' }, - { cols: 1, rows: 1, x: 3, y: 0, name: 'i18n:dashboard.githubCard', type: 'github' }, - - // Row 2 - { cols: 2, rows: 1, x: 0, y: 1, name: 'i18n:dashboard.apiCallsChart', type: 'api-calls' }, - { cols: 2, rows: 1, x: 2, y: 1, name: 'i18n:dashboard.apiPerformanceChart', type: 'api-performance' }, - - // Row 3 - { cols: 1, rows: 1, x: 0, y: 2, name: 'i18n:dashboard.apiCallsSummaryCard', type: 'api-calls-summary' }, - { cols: 2, rows: 1, x: 1, y: 2, name: 'i18n:dashboard.assetUpdloadsCountChart', type: 'asset-uploads-count' }, - { cols: 1, rows: 1, x: 2, y: 2, name: 'i18n:dashboard.assetUploadsSizeChart', type: 'asset-uploads-size-summary' }, - - // Row 4 - { cols: 2, rows: 1, x: 0, y: 3, name: 'i18n:dashboard.assetTotalSize', type: 'asset-uploads-size' }, - { cols: 2, rows: 1, x: 2, y: 3, name: 'i18n:dashboard.trafficChart', type: 'api-traffic' }, - - // Row 5 - { cols: 1, rows: 1, x: 0, y: 4, name: 'i18n:dashboard.trafficSummaryCard', type: 'api-traffic-summary' }, - { cols: 2, rows: 1, x: 1, y: 4, name: 'i18n:dashboard.historyCard', type: 'history' }, -]; - -const OPTIONAL_CARDS = [ - { cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.randomCatCard', type: 'random-cat' }, - { cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.randomDogCard', type: 'random-dog' }, - { cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.contentSummaryCard', type: 'content-summary' }, -]; diff --git a/frontend/src/app/features/dashboard/pages/dashboard-page.component.html b/frontend/src/app/features/dashboard/pages/dashboard-page.component.html index 2d3878654..1124074df 100644 --- a/frontend/src/app/features/dashboard/pages/dashboard-page.component.html +++ b/frontend/src/app/features/dashboard/pages/dashboard-page.component.html @@ -17,38 +17,37 @@ - - - - + - + - + - + - + - + - - @@ -59,6 +58,9 @@ + + + @@ -66,7 +68,7 @@ - + @@ -75,9 +77,13 @@
- +
diff --git a/frontend/src/app/features/dashboard/pages/dashboard-page.component.ts b/frontend/src/app/features/dashboard/pages/dashboard-page.component.ts index 7a642f6da..d4cab922d 100644 --- a/frontend/src/app/features/dashboard/pages/dashboard-page.component.ts +++ b/frontend/src/app/features/dashboard/pages/dashboard-page.component.ts @@ -32,6 +32,8 @@ export class DashboardPageComponent extends ResourceOwner implements AfterViewIn public callsUsage?: CallsUsageDto; public gridConfig?: GridsterItem[]; + public gridConfigAvailable = [...DEFAULT_CELLS, ...ADDITIONAL_CELLS]; + public gridConfigDefaults = DEFAULT_CELLS; public gridOptions = DEFAULT_OPTIONS; public extendedHeight: string = ''; @@ -107,6 +109,37 @@ export class DashboardPageComponent extends ResourceOwner implements AfterViewIn } } +const DEFAULT_CELLS: GridsterItem[] = [ + // Row 1 + { cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.schemasCard', type: 'schemas' }, + { cols: 1, rows: 1, x: 1, y: 0, name: 'i18n:dashboard.apiDocumentationCard', type: 'api' }, + { cols: 1, rows: 1, x: 2, y: 0, name: 'i18n:dashboard.supportCard', type: 'support' }, + { cols: 1, rows: 1, x: 3, y: 0, name: 'i18n:dashboard.githubCard', type: 'github' }, + + // Row 2 + { cols: 2, rows: 1, x: 0, y: 1, name: 'i18n:dashboard.apiCallsChart', type: 'api-calls' }, + { cols: 2, rows: 1, x: 2, y: 1, name: 'i18n:dashboard.apiPerformanceChart', type: 'api-performance' }, + + // Row 3 + { cols: 1, rows: 1, x: 0, y: 2, name: 'i18n:dashboard.apiCallsSummaryCard', type: 'api-calls-summary' }, + { cols: 2, rows: 1, x: 1, y: 2, name: 'i18n:dashboard.assetUpdloadsCountChart', type: 'asset-uploads-count' }, + { cols: 1, rows: 1, x: 2, y: 2, name: 'i18n:dashboard.assetUploadsSizeChart', type: 'asset-uploads-size-summary' }, + + // Row 4 + { cols: 2, rows: 1, x: 0, y: 3, name: 'i18n:dashboard.assetTotalSize', type: 'asset-uploads-size' }, + { cols: 2, rows: 1, x: 2, y: 3, name: 'i18n:dashboard.trafficChart', type: 'api-traffic' }, + + // Row 5 + { cols: 1, rows: 1, x: 0, y: 4, name: 'i18n:dashboard.trafficSummaryCard', type: 'api-traffic-summary' }, + { cols: 2, rows: 1, x: 1, y: 4, name: 'i18n:dashboard.historyCard', type: 'history' }, +]; + +const ADDITIONAL_CELLS = [ + { cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.randomCatCard', type: 'random-cat' }, + { cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.randomDogCard', type: 'random-dog' }, + { cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.contentSummaryCard', type: 'content-summary' }, +]; + const DEFAULT_OPTIONS: GridsterConfig = { displayGrid: 'onDrag&Resize', draggable: { diff --git a/frontend/src/app/features/settings/declarations.ts b/frontend/src/app/features/settings/declarations.ts index 576936dd7..96279d79e 100644 --- a/frontend/src/app/features/settings/declarations.ts +++ b/frontend/src/app/features/settings/declarations.ts @@ -35,3 +35,4 @@ export * from './pages/workflows/workflow-transition.component'; export * from './pages/workflows/workflow.component'; export * from './pages/workflows/workflows-page.component'; export * from './settings-area.component'; +export * from './settings-menu.component'; diff --git a/frontend/src/app/features/settings/module.ts b/frontend/src/app/features/settings/module.ts index 5bff29a79..67530d757 100644 --- a/frontend/src/app/features/settings/module.ts +++ b/frontend/src/app/features/settings/module.ts @@ -8,7 +8,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HelpComponent, HistoryComponent, SqxFrameworkModule, SqxSharedModule } from '@app/shared'; -import { AssetScriptsPageComponent, BackupComponent, BackupsPageComponent, ClientAddFormComponent, ClientComponent, ClientConnectFormComponent, ClientsPageComponent, ContributorAddFormComponent, ContributorComponent, ContributorsPageComponent, ImportContributorsDialogComponent, LanguageAddFormComponent, LanguageComponent, LanguagesPageComponent, MorePageComponent, PlanComponent, PlansPageComponent, RoleAddFormComponent, RoleComponent, RolesPageComponent, SettingsAreaComponent, SettingsPageComponent, TemplateComponent, TemplatesPageComponent, WorkflowAddFormComponent, WorkflowComponent, WorkflowDiagramComponent, WorkflowsPageComponent, WorkflowStepComponent, WorkflowTransitionComponent } from './declarations'; +import { AssetScriptsPageComponent, BackupComponent, BackupsPageComponent, ClientAddFormComponent, ClientComponent, ClientConnectFormComponent, ClientsPageComponent, ContributorAddFormComponent, ContributorComponent, ContributorsPageComponent, ImportContributorsDialogComponent, LanguageAddFormComponent, LanguageComponent, LanguagesPageComponent, MorePageComponent, PlanComponent, PlansPageComponent, RoleAddFormComponent, RoleComponent, RolesPageComponent, SettingsAreaComponent, SettingsMenuComponent, SettingsPageComponent, TemplateComponent, TemplatesPageComponent, WorkflowAddFormComponent, WorkflowComponent, WorkflowDiagramComponent, WorkflowsPageComponent, WorkflowStepComponent, WorkflowTransitionComponent } from './declarations'; const routes: Routes = [ { @@ -191,6 +191,7 @@ const routes: Routes = [ RoleComponent, RolesPageComponent, SettingsAreaComponent, + SettingsMenuComponent, SettingsPageComponent, TemplateComponent, TemplatesPageComponent, diff --git a/frontend/src/app/features/settings/pages/more/more-page.component.html b/frontend/src/app/features/settings/pages/more/more-page.component.html index 788540e67..5bb477762 100644 --- a/frontend/src/app/features/settings/pages/more/more-page.component.html +++ b/frontend/src/app/features/settings/pages/more/more-page.component.html @@ -46,7 +46,7 @@
-
+
@@ -55,7 +55,7 @@
- + @@ -72,7 +72,7 @@
{{ 'apps.uploadImage' | sqxTranslate }} - + {{ 'apps.uploadImageButton' | sqxTranslate }} @@ -85,6 +85,29 @@
{{ 'apps.generalSettingsDangerZone' | sqxTranslate }}
+
+
+
+
{{ 'apps.transferTitle' | sqxTranslate }}
+ + {{ 'apps.transferWarning' | sqxTranslate }} +
+
+ +
+
+ +
+
+
+ +
+
{{ 'apps.delete' | sqxTranslate }}
diff --git a/frontend/src/app/features/settings/pages/more/more-page.component.scss b/frontend/src/app/features/settings/pages/more/more-page.component.scss index 7a9168cbe..315b6a221 100644 --- a/frontend/src/app/features/settings/pages/more/more-page.component.scss +++ b/frontend/src/app/features/settings/pages/more/more-page.component.scss @@ -1,6 +1,11 @@ @import 'mixins'; @import 'vars'; +hr { + margin-bottom: 2rem; + margin-top: 2rem; +} + .app { &-image { min-height: 150px; diff --git a/frontend/src/app/features/settings/pages/more/more-page.component.ts b/frontend/src/app/features/settings/pages/more/more-page.component.ts index 43bf93021..a57342989 100644 --- a/frontend/src/app/features/settings/pages/more/more-page.component.ts +++ b/frontend/src/app/features/settings/pages/more/more-page.component.ts @@ -7,7 +7,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; -import { AppDto, AppsState, defined, ResourceOwner, Types, UpdateAppForm } from '@app/shared'; +import { AppDto, AppsState, defined, DialogService, ResourceOwner, TeamsState, TransferAppForm, Types, UpdateAppForm } from '@app/shared'; @Component({ selector: 'sqx-more-page', @@ -17,18 +17,25 @@ import { AppDto, AppsState, defined, ResourceOwner, Types, UpdateAppForm } from export class MorePageComponent extends ResourceOwner implements OnInit { public app!: AppDto; + public teams: { id: string | null; name: string }[] = []; + public isEditable = false; - public isImageEditable = false; + public isEditableImage = false; public isDeletable = false; + public isTransferable = false; public uploading = false; public uploadProgress = 10; + public transferForm = new TransferAppForm(); + public updateForm = new UpdateAppForm(); constructor( private readonly appsState: AppsState, + private readonly dialogs: DialogService, private readonly router: Router, + public readonly teamsState: TeamsState, ) { super(); } @@ -41,10 +48,14 @@ export class MorePageComponent extends ResourceOwner implements OnInit { this.isDeletable = app.canDelete; this.isEditable = app.canUpdateGeneral; - this.isImageEditable = app.canUpdateImage; + this.isEditableImage = app.canUpdateImage; + this.isTransferable = app.canUpdateTeam; this.updateForm.load(app); this.updateForm.setEnabled(this.isEditable); + + this.transferForm.load(app); + this.transferForm.setEnabled(this.isTransferable); })); this.appsState.reloadApps(); @@ -64,14 +75,38 @@ export class MorePageComponent extends ResourceOwner implements OnInit { this.updateForm.submitCompleted({ newValue: app }); }, error: error => { + this.dialogs.notifyError(error); + this.updateForm.submitFailed(error); }, }); } } + public transfer() { + if (!this.isTransferable) { + return; + } + + const value = this.transferForm.submit(); + + if (value) { + this.appsState.transfer(this.app, value.teamId) + .subscribe({ + next: app => { + this.transferForm.submitCompleted({ newValue: app }); + }, + error: error => { + this.dialogs.notifyError(error); + + this.transferForm.submitFailed(error); + }, + }); + } + } + public uploadImage(file: ReadonlyArray) { - if (!this.isImageEditable) { + if (!this.isEditableImage) { return; } @@ -95,7 +130,7 @@ export class MorePageComponent extends ResourceOwner implements OnInit { } public removeImage() { - if (!this.isImageEditable) { + if (!this.isEditableImage) { return; } diff --git a/frontend/src/app/features/settings/pages/plans/plans-page.component.html b/frontend/src/app/features/settings/pages/plans/plans-page.component.html index 1cfcce805..a299d7bcc 100644 --- a/frontend/src/app/features/settings/pages/plans/plans-page.component.html +++ b/frontend/src/app/features/settings/pages/plans/plans-page.component.html @@ -16,6 +16,10 @@ {{ 'plans.notPlanOwner' | sqxTranslate }} {{ 'plans.planOwner' | sqxTranslate }}: {{plansState.planOwner | async | sqxUserName}}
+
+ {{ 'plans.managedByTeam' | sqxTranslate }} +
+
{{ 'plans.noPlanConfigured' | sqxTranslate }} diff --git a/frontend/src/app/features/settings/settings-area.component.html b/frontend/src/app/features/settings/settings-area.component.html index b5f5104f1..2c994c8b2 100644 --- a/frontend/src/app/features/settings/settings-area.component.html +++ b/frontend/src/app/features/settings/settings-area.component.html @@ -2,74 +2,7 @@ - + diff --git a/frontend/src/app/features/settings/settings-menu.component.html b/frontend/src/app/features/settings/settings-menu.component.html new file mode 100644 index 000000000..8fdc4e1b0 --- /dev/null +++ b/frontend/src/app/features/settings/settings-menu.component.html @@ -0,0 +1,68 @@ + \ No newline at end of file diff --git a/frontend/src/app/features/settings/settings-menu.component.scss b/frontend/src/app/features/settings/settings-menu.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/features/dashboard/pages/cards/support-card.component.ts b/frontend/src/app/features/settings/settings-menu.component.ts similarity index 65% rename from frontend/src/app/features/dashboard/pages/cards/support-card.component.ts rename to frontend/src/app/features/settings/settings-menu.component.ts index 1a088a3e2..080abc981 100644 --- a/frontend/src/app/features/dashboard/pages/cards/support-card.component.ts +++ b/frontend/src/app/features/settings/settings-menu.component.ts @@ -9,12 +9,12 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { AppDto } from '@app/shared'; @Component({ - selector: 'sqx-support-card[app]', - styleUrls: ['./support-card.component.scss'], - templateUrl: './support-card.component.html', + selector: 'sqx-settings-menu[app]', + styleUrls: ['./settings-menu.component.scss'], + templateUrl: './settings-menu.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SupportCardComponent { +export class SettingsMenuComponent { @Input() public app!: AppDto; } diff --git a/frontend/src/app/features/teams/declarations.ts b/frontend/src/app/features/teams/declarations.ts new file mode 100644 index 000000000..132bf4c76 --- /dev/null +++ b/frontend/src/app/features/teams/declarations.ts @@ -0,0 +1,20 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +export * from './left-menu.component'; +export * from './pages/contributors/contributor-add-form.component'; +export * from './pages/contributors/contributor.component'; +export * from './pages/contributors/contributors-page.component'; +export * from './pages/contributors/import-contributors-dialog.component'; +export * from './pages/dashboard/cards/apps-card.component'; +export * from './pages/dashboard/dashboard-page.component'; +export * from './pages/more/more-page.component'; +export * from './pages/plans/plan.component'; +export * from './pages/plans/plans-page.component'; +export * from './shared/settings-area.component'; +export * from './shared/settings-menu.component'; +export * from './team-area.component'; \ No newline at end of file diff --git a/frontend/src/app/features/teams/internal.ts b/frontend/src/app/features/teams/internal.ts new file mode 100644 index 000000000..4bd7906d5 --- /dev/null +++ b/frontend/src/app/features/teams/internal.ts @@ -0,0 +1,12 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +export * from './services/team-contributors.service'; +export * from './services/team-plans.service'; +export * from './state/team-contributors.forms'; +export * from './state/team-contributors.state'; +export * from './state/team-plans.state'; \ No newline at end of file diff --git a/frontend/src/app/features/teams/left-menu.component.html b/frontend/src/app/features/teams/left-menu.component.html new file mode 100644 index 000000000..394a37e22 --- /dev/null +++ b/frontend/src/app/features/teams/left-menu.component.html @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/frontend/src/app/features/teams/left-menu.component.scss b/frontend/src/app/features/teams/left-menu.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/features/teams/left-menu.component.ts b/frontend/src/app/features/teams/left-menu.component.ts new file mode 100644 index 000000000..943cb5b07 --- /dev/null +++ b/frontend/src/app/features/teams/left-menu.component.ts @@ -0,0 +1,20 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TeamDto } from '@app/shared'; + +@Component({ + selector: 'sqx-left-menu[team]', + styleUrls: ['./left-menu.component.scss'], + templateUrl: './left-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LeftMenuComponent { + @Input() + public team!: TeamDto; +} diff --git a/frontend/src/app/features/teams/module.ts b/frontend/src/app/features/teams/module.ts new file mode 100644 index 000000000..b39f6bf2b --- /dev/null +++ b/frontend/src/app/features/teams/module.ts @@ -0,0 +1,100 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { GridsterModule } from 'angular-gridster2'; +import { HelpComponent, HistoryComponent, SqxFrameworkModule, SqxSharedModule } from '@app/shared'; +import { AppsCardComponent, ContributorAddFormComponent, ContributorComponent, ContributorsPageComponent, DashboardPageComponent, ImportContributorsDialogComponent, LeftMenuComponent, MorePageComponent, PlanComponent, PlansPageComponent, SettingsAreaComponent, SettingsMenuComponent, TeamAreaComponent } from './declarations'; +import { TeamContributorsService, TeamContributorsState, TeamPlansService, TeamPlansState } from './internal'; + +const routes: Routes = [ + { + path: '', + component: TeamAreaComponent, + children: [ + { + path: '', + component: DashboardPageComponent, + }, + { + path: 'settings', + component: SettingsAreaComponent, + children: [ + { + path: 'contributors', + component: ContributorsPageComponent, + children: [ + { + path: 'history', + component: HistoryComponent, + data: { + channel: 'settings.contributors', + }, + }, + { + path: 'help', + component: HelpComponent, + data: { + helpPage: '05-integrated/team-contributors', + }, + }, + ], + }, + { + path: 'plans', + component: PlansPageComponent, + children: [ + { + path: 'history', + component: HistoryComponent, + data: { + channel: 'settings.plan', + }, + }, + ], + }, + { + path: 'more', + component: MorePageComponent, + }, + ], + }, + ], + }, +]; + +@NgModule({ + imports: [ + GridsterModule, + RouterModule.forChild(routes), + SqxFrameworkModule, + SqxSharedModule, + ], + declarations: [ + AppsCardComponent, + DashboardPageComponent, + ContributorAddFormComponent, + ContributorComponent, + ContributorsPageComponent, + ImportContributorsDialogComponent, + LeftMenuComponent, + MorePageComponent, + PlanComponent, + PlansPageComponent, + SettingsAreaComponent, + SettingsMenuComponent, + TeamAreaComponent, + ], + providers: [ + TeamContributorsService, + TeamContributorsState, + TeamPlansService, + TeamPlansState, + ], +}) +export class SqxFeatureTeamsModule {} diff --git a/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.html b/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.html new file mode 100644 index 000000000..91133f4bb --- /dev/null +++ b/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.html @@ -0,0 +1,36 @@ +
+
{{ 'contributors.add.title' | sqxTranslate }}
+ +
+
+
+ + + + + + {{user.displayName}} + + + +
+
+ +
+
+
+ +
+ + {{ 'contributors.importHintg' | sqxTranslate }} {{ 'contributors.importButton' | sqxTranslate }} + +
+
+ + + + + \ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.scss b/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.scss new file mode 100644 index 000000000..2b6826a9a --- /dev/null +++ b/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.scss @@ -0,0 +1,14 @@ +@import 'mixins'; +@import 'vars'; + +.autocomplete-user { + @include truncate; + + .user-name { + margin-left: .25rem; + } +} + +.table-items-header { + margin: 0; +} \ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.ts b/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.ts new file mode 100644 index 000000000..4d0b601c0 --- /dev/null +++ b/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.ts @@ -0,0 +1,82 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { withLatestFrom } from 'rxjs/operators'; +import { AssignTeamContributorForm, TeamContributorsState } from '@app/features/teams/internal'; +import { AutocompleteSource, DialogModel, DialogService, UsersService } from '@app/shared'; + +@Injectable() +export class UsersDataSource implements AutocompleteSource { + constructor( + private readonly contributorsState: TeamContributorsState, + private readonly usersService: UsersService, + ) { + } + + public find(query: string): Observable> { + if (!query) { + return of([]); + } + + return this.usersService.getUsers(query).pipe( + withLatestFrom(this.contributorsState.contributors, (users, contributors) => { + const results: any[] = []; + + for (const user of users) { + if (!contributors!.find(t => t.contributorId === user.id)) { + results.push(user); + } + } + return results; + })); + } +} + +@Component({ + selector: 'sqx-contributor-add-form', + styleUrls: ['./contributor-add-form.component.scss'], + templateUrl: './contributor-add-form.component.html', + providers: [ + UsersDataSource, + ], +}) +export class ContributorAddFormComponent { + public assignContributorForm = new AssignTeamContributorForm(); + + public importDialog = new DialogModel(); + + constructor( + public readonly contributorsState: TeamContributorsState, + public readonly usersDataSource: UsersDataSource, + private readonly dialogs: DialogService, + ) { + } + + public assignContributor() { + const value = this.assignContributorForm.submit(); + + if (value) { + this.contributorsState.assign(value) + .subscribe({ + next: isCreated => { + this.assignContributorForm.submitCompleted({ newValue: { user: '' } as any }); + + if (isCreated) { + this.dialogs.notifyInfo('i18n:contributors.contributorAssigned'); + } else { + this.dialogs.notifyInfo('i18n:contributors.contributorAssignedOld'); + } + }, + error: error => { + this.assignContributorForm.submitFailed(error); + }, + }); + } + } +} diff --git a/frontend/src/app/features/teams/pages/contributors/contributor.component.html b/frontend/src/app/features/teams/pages/contributors/contributor.component.html new file mode 100644 index 000000000..8c83c0714 --- /dev/null +++ b/frontend/src/app/features/teams/pages/contributors/contributor.component.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/features/dashboard/pages/cards/api-calls-card.component.scss b/frontend/src/app/features/teams/pages/contributors/contributor.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/api-calls-card.component.scss rename to frontend/src/app/features/teams/pages/contributors/contributor.component.scss diff --git a/frontend/src/app/features/teams/pages/contributors/contributor.component.ts b/frontend/src/app/features/teams/pages/contributors/contributor.component.ts new file mode 100644 index 000000000..affc6ddfd --- /dev/null +++ b/frontend/src/app/features/teams/pages/contributors/contributor.component.ts @@ -0,0 +1,37 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TeamContributorsState } from '@app/features/teams/internal'; +import { ContributorDto } from '@app/shared'; + +@Component({ + selector: '[sqxContributor]', + styleUrls: ['./contributor.component.scss'], + templateUrl: './contributor.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ContributorComponent { + @Input() + public search?: string | RegExp | null; + + @Input('sqxContributor') + public contributor!: ContributorDto; + + constructor( + private readonly contributorsState: TeamContributorsState, + ) { + } + + public remove() { + this.contributorsState.revoke(this.contributor); + } + + public changeRole(role: string) { + this.contributorsState.assign({ contributorId: this.contributor.contributorId, role }); + } +} diff --git a/frontend/src/app/features/teams/pages/contributors/contributors-page.component.html b/frontend/src/app/features/teams/pages/contributors/contributors-page.component.html new file mode 100644 index 000000000..4fe474c9e --- /dev/null +++ b/frontend/src/app/features/teams/pages/contributors/contributors-page.component.html @@ -0,0 +1,61 @@ + + + + +
+ + + + +
+ +
+
+
+ + + + + + + + + +
+ {{ 'contributors.empty' | sqxTranslate }} +
+ + + + +
+
+
+ + + + +
+
+ + +
+ + + + + + + +
+
+
+ + \ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/contributors/contributors-page.component.scss b/frontend/src/app/features/teams/pages/contributors/contributors-page.component.scss new file mode 100644 index 000000000..bbc8a22a2 --- /dev/null +++ b/frontend/src/app/features/teams/pages/contributors/contributors-page.component.scss @@ -0,0 +1,6 @@ +@import 'mixins'; +@import 'vars'; + +.import-hint { + padding-right: $panel-padding; +} \ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/contributors/contributors-page.component.ts b/frontend/src/app/features/teams/pages/contributors/contributors-page.component.ts new file mode 100644 index 000000000..d06451873 --- /dev/null +++ b/frontend/src/app/features/teams/pages/contributors/contributors-page.component.ts @@ -0,0 +1,48 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, OnInit } from '@angular/core'; +import { TeamContributorsState } from '@app/features/teams/internal'; +import { ContributorDto, Router2State } from '@app/shared'; + +@Component({ + selector: 'sqx-contributors-page', + styleUrls: ['./contributors-page.component.scss'], + templateUrl: './contributors-page.component.html', + providers: [ + Router2State, + ], +}) +export class ContributorsPageComponent implements OnInit { + constructor( + public readonly contributorsRoute: Router2State, + public readonly contributorsState: TeamContributorsState, + ) { + } + + public ngOnInit() { + const initial = + this.contributorsRoute.mapTo(this.contributorsState) + .withPaging('contributors', 10) + .withString('query') + .getInitial(); + + this.contributorsState.load(false, initial); + } + + public reload() { + this.contributorsState.load(true); + } + + public search(query: string) { + this.contributorsState.search(query); + } + + public trackByContributor(_index: number, contributor: ContributorDto) { + return contributor.contributorId; + } +} diff --git a/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.html b/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.html new file mode 100644 index 000000000..b0b941b4e --- /dev/null +++ b/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.html @@ -0,0 +1,63 @@ +
+ + + {{ 'contributors.importTitle' | sqxTranslate }} + + + + + + + + + + {{ 'contributors.import.emailsDetected' | sqxTranslate: { count: count } }} + + +   + + + + +
+
+ {{status.email}} +
+
+
+ + +
+
+ {{status.email}} +
+ +
+ +
+
+
+
+
+ + + + + + + + + + + + + + +
+
diff --git a/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.scss b/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.scss new file mode 100644 index 000000000..9e75c9af9 --- /dev/null +++ b/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.scss @@ -0,0 +1,14 @@ +@import 'mixins'; +@import 'vars'; + +textarea { + resize: none; +} + +.aligned { + line-height: 2.5rem; +} + +.content { + min-height: 300px; +} \ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.ts b/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.ts new file mode 100644 index 000000000..cb262f1ca --- /dev/null +++ b/frontend/src/app/features/teams/pages/contributors/import-contributors-dialog.component.ts @@ -0,0 +1,95 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, EventEmitter, Output } from '@angular/core'; +import { EMPTY, of } from 'rxjs'; +import { catchError, mergeMap, tap } from 'rxjs/operators'; +import { TeamContributorsState } from '@app/features/teams/internal'; +import { ErrorDto, ImportContributorsForm } from '@app/shared'; + +type ImportStatus = { + email: string; + result: 'Pending' | 'Failed' | 'Success'; + resultText: string; + role: string; +}; + +@Component({ + selector: 'sqx-import-contributors-dialog', + styleUrls: ['./import-contributors-dialog.component.scss'], + templateUrl: './import-contributors-dialog.component.html', +}) +export class ImportContributorsDialogComponent { + @Output() + public close = new EventEmitter(); + + public importForm = new ImportContributorsForm(); + public importStatus: ReadonlyArray = []; + public importStage: 'Start' | 'Change' | 'Wait' = 'Start'; + + constructor( + private readonly contributorsState: TeamContributorsState, + ) { + } + + public detect() { + this.importStage = 'Change'; + + const contributors = this.importForm.submit(); + + if (contributors) { + this.importStatus = contributors.map(contributor => ({ + email: contributor.contributorId, + result: 'Pending', + resultText: 'Pending', + role: 'Developer', + })); + } + } + + public import() { + this.importStage = 'Wait'; + + of(...this.importStatus).pipe( + mergeMap(s => + this.contributorsState.assign(createRequest(s), { silent: true }).pipe( + tap(created => { + const status = this.importStatus.find(x => x.email === s.email); + + if (status) { + status.resultText = getSuccess(created); + status.result = 'Success'; + } + }), + catchError((error: ErrorDto) => { + const status = this.importStatus.find(x => x.email === s.email); + + if (status) { + status.resultText = getError(error); + status.result = 'Failed'; + } + + return EMPTY; + }), + ), 1), + ).subscribe(); + } +} + +function createRequest(status: ImportStatus) { + return { contributorId: status.email, role: status.role, invite: true }; +} + +function getError(error: ErrorDto): string { + return error.details[0].originalMessage; +} + +function getSuccess(created: boolean | undefined): string { + return created ? + 'i18n:contributors.contributorAssignedInvited' : + 'i18n:contributors.contributorAssignedExisting'; +} diff --git a/frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.html b/frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.html new file mode 100644 index 000000000..6b32b199b --- /dev/null +++ b/frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.html @@ -0,0 +1,13 @@ +
+
{{ 'common.apps' | sqxTranslate }}
+
+
+
+ {{app.displayName}} +
+ +
+
+
\ No newline at end of file diff --git a/frontend/src/app/features/dashboard/pages/cards/api-calls-summary-card.component.scss b/frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/api-calls-summary-card.component.scss rename to frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.scss diff --git a/frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.ts b/frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.ts new file mode 100644 index 000000000..6e79216dc --- /dev/null +++ b/frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.ts @@ -0,0 +1,40 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { AppDto, AppsService, StatefulComponent, TeamDto } from '@app/shared'; + +interface State { + // The apps for this team. + apps: ReadonlyArray; +} + +@Component({ + selector: 'sqx-apps-card[team]', + styleUrls: ['./apps-card.component.scss'], + templateUrl: './apps-card.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppsCardComponent extends StatefulComponent implements OnInit { + @Input() + public team!: TeamDto; + + constructor(changeDetector: ChangeDetectorRef, + private readonly appsService: AppsService, + ) { + super(changeDetector, { + apps: [], + }); + } + + public ngOnInit() { + this.appsService.getTeamApps(this.team.id) + .subscribe(apps => { + this.next({ apps }); + }); + } +} diff --git a/frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.html b/frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.html new file mode 100644 index 000000000..07134dc71 --- /dev/null +++ b/frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.html @@ -0,0 +1,67 @@ + + + +
+
+

{{ 'dashboard.welcomeTitle' | sqxTranslate: { user: user } }}

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.scss b/frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.scss new file mode 100644 index 000000000..bbd6fc791 --- /dev/null +++ b/frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.scss @@ -0,0 +1,133 @@ +@import 'mixins'; +@import 'vars'; + +gridster-item { + position: absolute; +} + +.dashboard { + @include absolute(0, 0, 0, 0); + + &-summary { + @include absolute(2rem, null, null, 16px); + z-index: 0; + } + + &-settings { + @include absolute(1rem, 1rem, null, null); + z-index: 1000; + } + + &-title { + font-size: 1.4rem; + } + + /* stylelint-disable */ + gridster { + background: none; + display: flex; + flex-direction: row; + flex-grow: 1; + + &::after { + @include absolute(100%, 0, null, 0); + content: ''; + padding: 0; + padding-bottom: var(--gridster-margin); + pointer-events: none; + } + } + + gridster-item { + background: none; + border: 0; + border-radius: 0; + overflow: visible; + + &.gridster-item-moving, + &.gridster-item-resizing { + box-shadow: none; + } + } + /* stylelint-enable */ +} + +.btn { + z-index: 1000; +} + +:host ::ng-deep { + /* stylelint-disable */ + chart { + @include absolute(0, 1rem, 1rem, 1rem); + } + /* stylelint-enable */ + + .subtext { + margin-bottom: 2rem; + } + + .card { + @include absolute(0, 0, 0, 0); + + &-image { + text-align: center; + + img { + height: 5rem; + } + } + + h4 { + a { + color: $color-title; + text-align: left; + text-decoration: none; + } + } + + &-header { + @include truncate; + flex-grow: 0; + flex-shrink: 0; + } + + &-body { + position: relative; + } + + &-title { + margin-top: 1rem; + } + + &-history { + overflow-y: auto; + } + + &-text { + color: $color-text-decent; + font-size: $font-small; + font-weight: normal; + } + + &-href { + &:hover { + @include box-shadow-outer(0, 3px, 16px, .2); + } + } + } + + .aggregation { + text-align: center; + + &-label { + color: $color-text-decent; + } + + &-value { + font-size: 3rem; + margin-bottom: .5rem; + margin-top: 1rem; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.ts b/frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.ts new file mode 100644 index 000000000..18fdc2483 --- /dev/null +++ b/frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.ts @@ -0,0 +1,156 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { GridsterComponent, GridsterConfig, GridsterItem, GridType } from 'angular-gridster2'; +import { AuthService, CallsUsageDto, CurrentStorageDto, DateTime, defined, fadeAnimation, LocalStoreService, ResourceOwner, Settings, StorageUsagePerDateDto, switchSafe, TeamsState, UsagesService } from '@app/shared'; + +@Component({ + selector: 'sqx-dashboard-page', + styleUrls: ['./dashboard-page.component.scss'], + templateUrl: './dashboard-page.component.html', + animations: [ + fadeAnimation, + ], +}) +export class DashboardPageComponent extends ResourceOwner implements AfterViewInit, OnInit { + @ViewChild('grid') + public grid!: GridsterComponent; + + public selectedTeam = this.teamsState.selectedTeam.pipe(defined()); + + public isStacked = false; + public isScrolled = false; + + public storageCurrent?: CurrentStorageDto; + public storageUsage?: ReadonlyArray; + + public callsUsage?: CallsUsageDto; + + public gridConfig: GridsterItem[] = DEFAULT_CONFIG; + public gridOptions = DEFAULT_OPTIONS; + public gridDefault = DEFAULT_CONFIG; + public gridOptionals = OPTIONAL_CARDS; + + public extendedHeight: string = ''; + + public user = this.authState.user?.displayName; + + constructor( + private readonly authState: AuthService, + private readonly localStore: LocalStoreService, + private readonly renderer: Renderer2, + private readonly teamsState: TeamsState, + private readonly usagesService: UsagesService, + private readonly zone: NgZone, + ) { + super(); + + this.isStacked = localStore.getBoolean(Settings.Local.DASHBOARD_CHART_STACKED); + } + + public ngOnInit() { + const dateTo = DateTime.today().toStringFormat('yyyy-MM-dd'); + const dateFrom = DateTime.today().addDays(-20).toStringFormat('yyyy-MM-dd'); + + this.own( + this.selectedTeam.pipe(switchSafe(team => this.usagesService.getTodayStorageForTeam(team.id))) + .subscribe(dto => { + this.storageCurrent = dto; + })); + + this.own( + this.selectedTeam.pipe(switchSafe(team => this.usagesService.getStorageUsagesForTeam(team.id, dateFrom, dateTo))) + .subscribe(dtos => { + this.storageUsage = dtos; + })); + + this.own( + this.selectedTeam.pipe(switchSafe(team => this.usagesService.getCallsUsagesForTeam(team.id, dateFrom, dateTo))) + .subscribe(dto => { + this.callsUsage = dto; + })); + } + + public ngAfterViewInit() { + this.zone.runOutsideAngular(() => { + const gridElement = this.grid.el; + + this.renderer.listen(gridElement, 'scroll', () => { + const isScrolled = gridElement.scrollTop > 0; + + if (isScrolled !== this.isScrolled) { + this.zone.run(() => { + this.isScrolled = isScrolled; + }); + } + }); + }); + } + + public ngAfterViewChecked() { + this.extendedHeight = `${this.grid.gridRows.length * this.grid.curRowHeight - (this.gridOptions.margin || 0)}px`; + } + + public changeIsStacked(value: boolean) { + this.localStore.setBoolean(Settings.Local.DASHBOARD_CHART_STACKED, value); + + this.isStacked = value; + } + + public changeConfig(config: GridsterItem[]) { + this.gridConfig = config; + + this.grid?.updateGrid(); + } +} + +const DEFAULT_CONFIG: GridsterItem[] = [ + // Row 1 + { cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.trafficSummaryCard', type: 'api-traffic-summary' }, + { cols: 2, rows: 1, x: 1, y: 0, name: 'i18n:dashboard.appsCard', type: 'apps' }, + { cols: 1, rows: 1, x: 3, y: 0, name: 'i18n:dashboard.supportCard', type: 'support' }, + + // Row 2 + { cols: 2, rows: 1, x: 0, y: 1, name: 'i18n:dashboard.apiCallsChart', type: 'api-calls' }, + { cols: 2, rows: 1, x: 2, y: 1, name: 'i18n:dashboard.apiPerformanceChart', type: 'api-performance' }, + + // Row 3 + { cols: 1, rows: 1, x: 0, y: 2, name: 'i18n:dashboard.apiCallsSummaryCard', type: 'api-calls-summary' }, + { cols: 2, rows: 1, x: 1, y: 2, name: 'i18n:dashboard.assetUpdloadsCountChart', type: 'asset-uploads-count' }, + { cols: 1, rows: 1, x: 2, y: 2, name: 'i18n:dashboard.assetUploadsSizeChart', type: 'asset-uploads-size-summary' }, + + // Row 4 + { cols: 2, rows: 1, x: 0, y: 3, name: 'i18n:dashboard.assetTotalSize', type: 'asset-uploads-size' }, + { cols: 2, rows: 1, x: 2, y: 3, name: 'i18n:dashboard.trafficChart', type: 'api-traffic' }, +]; + +const OPTIONAL_CARDS = [ + { cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.randomCatCard', type: 'random-cat' }, + { cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.randomDogCard', type: 'random-dog' }, +]; + +const DEFAULT_OPTIONS: GridsterConfig = { + displayGrid: 'onDrag&Resize', + draggable: { + enabled: true, + }, + fixedColWidth: 254, + fixedRowHeight: 254, + gridType: GridType.Fixed, + margin: 10, + maxItemCols: 3, + maxItemRows: 2, + outerMargin: true, + outerMarginBottom: 16, + outerMarginLeft: 16, + outerMarginRight: 16, + outerMarginTop: 120, + resizable: { + enabled: true, + }, +}; diff --git a/frontend/src/app/features/teams/pages/more/more-page.component.html b/frontend/src/app/features/teams/pages/more/more-page.component.html new file mode 100644 index 000000000..d510fe973 --- /dev/null +++ b/frontend/src/app/features/teams/pages/more/more-page.component.html @@ -0,0 +1,31 @@ + + + + +
+
{{ 'apps.generalSettings' | sqxTranslate }}
+ +
+
+ + +
+ + + + + +
+
+ + +
+
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/more/more-page.component.scss b/frontend/src/app/features/teams/pages/more/more-page.component.scss new file mode 100644 index 000000000..7a9168cbe --- /dev/null +++ b/frontend/src/app/features/teams/pages/more/more-page.component.scss @@ -0,0 +1,70 @@ +@import 'mixins'; +@import 'vars'; + +.app { + &-image { + min-height: 150px; + min-width: 150px; + position: relative; + } + + &-image-remove { + @include absolute(auto, .5rem, .5rem, auto); + } + + &-progress { + @include absolute(1rem, 1rem, 1rem, 1rem); + } +} + +.card-header, +.card-footer { + padding: 1.25rem; +} + +@mixin overlay { + @include absolute(0, 0, 0, 0); + color: $color-white; + display: flex; + opacity: 0; + transition: opacity .4s ease; + + &-background { + @include absolute(0, 0, 0, 0); + background: $color-black; + opacity: .7; + } +} + +.upload-button { + margin-top: 1rem; + + input { + @include hidden; + } +} + +.drop-overlay { + @include overlay; + pointer-events: none; + + &-text { + font-size: 1.25rem; + font-weight: normal; + position: absolute; + } +} + +.drag { + .drop-overlay { + opacity: 1; + } + + .app-image-remove { + display: none; + } +} + +.disabled { + pointer-events: none; +} \ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/more/more-page.component.ts b/frontend/src/app/features/teams/pages/more/more-page.component.ts new file mode 100644 index 000000000..6468bcda3 --- /dev/null +++ b/frontend/src/app/features/teams/pages/more/more-page.component.ts @@ -0,0 +1,63 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, OnInit } from '@angular/core'; +import { defined, ResourceOwner, TeamDto, TeamsState, UpdateTeamForm } from '@app/shared'; + +@Component({ + selector: 'sqx-more-page', + styleUrls: ['./more-page.component.scss'], + templateUrl: './more-page.component.html', +}) +export class MorePageComponent extends ResourceOwner implements OnInit { + public team!: TeamDto; + + public isEditable = false; + + public updateForm = new UpdateTeamForm(); + + constructor( + private readonly teamsState: TeamsState, + ) { + super(); + } + + public ngOnInit() { + this.own( + this.teamsState.selectedTeam.pipe(defined()) + .subscribe(team => { + this.team = team; + + this.isEditable = team.canUpdateGeneral; + + this.updateForm.load(team); + this.updateForm.setEnabled(this.isEditable); + })); + + this.teamsState.reloadTeams(); + } + + public save() { + if (!this.isEditable) { + return; + } + + const value = this.updateForm.submit(); + + if (value) { + this.teamsState.update(this.team, value) + .subscribe({ + next: team => { + this.updateForm.submitCompleted({ newValue: team }); + }, + error: error => { + this.updateForm.submitFailed(error); + }, + }); + } + } +} diff --git a/frontend/src/app/features/teams/pages/plans/plan.component.html b/frontend/src/app/features/teams/pages/plans/plan.component.html new file mode 100644 index 000000000..0a3019e03 --- /dev/null +++ b/frontend/src/app/features/teams/pages/plans/plan.component.html @@ -0,0 +1,56 @@ +
+
+

{{planInfo.plan.name}}

+
{{planInfo.plan.costs}}
+ + {{ 'plans.perMonth' | sqxTranslate }} +
+
+
+
+ {{planInfo.plan.maxApiCalls | sqxKNumber}} {{ 'plans.includedCalls' | sqxTranslate }} +
+
+ {{planInfo.plan.maxApiBytes | sqxFileSize}} {{ 'plans.includedTraffic' | sqxTranslate }} +
+
+ {{planInfo.plan.maxAssetSize | sqxFileSize}} {{ 'plans.includedStorage' | sqxTranslate }} +
+
+ {{planInfo.plan.maxContributors}} {{ 'plans.includedContributors' | sqxTranslate }} +
+
+ + + + +
+ +
\ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/plans/plan.component.scss b/frontend/src/app/features/teams/pages/plans/plan.component.scss new file mode 100644 index 000000000..cb9667486 --- /dev/null +++ b/frontend/src/app/features/teams/pages/plans/plan.component.scss @@ -0,0 +1,28 @@ +@import 'mixins'; +@import 'vars'; + +.plan { + &-price { + color: $color-theme-brand; + margin-bottom: 0; + margin-top: 0; + } + + &-selected { + pointer-events: none; + } + + &-fact { + line-height: 1.8rem; + } + + .btn { + margin-top: 1rem; + } +} + +.card-footer, +.card-header, +.card-body { + padding: 1rem; +} \ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/plans/plan.component.ts b/frontend/src/app/features/teams/pages/plans/plan.component.ts new file mode 100644 index 000000000..34234ec83 --- /dev/null +++ b/frontend/src/app/features/teams/pages/plans/plan.component.ts @@ -0,0 +1,34 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TeamPlansState } from '@app/features/teams/internal'; +import { PlanInfo } from '@app/shared'; + +@Component({ + selector: 'sqx-plan[planInfo]', + styleUrls: ['./plan.component.scss'], + templateUrl: './plan.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PlanComponent { + @Input() + public planInfo!: PlanInfo; + + constructor( + public readonly plansState: TeamPlansState, + ) { + } + + public changeMonthly() { + this.plansState.change(this.planInfo.plan.id); + } + + public changeYearly() { + this.plansState.change(this.planInfo.plan.yearlyId); + } +} diff --git a/frontend/src/app/features/teams/pages/plans/plans-page.component.html b/frontend/src/app/features/teams/pages/plans/plans-page.component.html new file mode 100644 index 000000000..8f915ff9a --- /dev/null +++ b/frontend/src/app/features/teams/pages/plans/plans-page.component.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + +
+ {{ 'plans.allApps' | sqxTranslate }} +
+ +
+
+ {{ 'plans.noPlanConfigured' | sqxTranslate }} +
+ +
+
+ +
+
+ + +
+
+
+
+ + +
+ + + +
+
+
+ + + diff --git a/frontend/src/app/features/teams/pages/plans/plans-page.component.scss b/frontend/src/app/features/teams/pages/plans/plans-page.component.scss new file mode 100644 index 000000000..10e73029b --- /dev/null +++ b/frontend/src/app/features/teams/pages/plans/plans-page.component.scss @@ -0,0 +1,15 @@ +@import 'mixins'; +@import 'vars'; + +.empty { + margin: 1.25rem; + margin-top: 6.25rem; +} + +.billing-portal-link { + padding: 2rem .5rem 0; +} + +.no-wrap { + white-space: nowrap; +} \ No newline at end of file diff --git a/frontend/src/app/features/teams/pages/plans/plans-page.component.ts b/frontend/src/app/features/teams/pages/plans/plans-page.component.ts new file mode 100644 index 000000000..ba1e038d0 --- /dev/null +++ b/frontend/src/app/features/teams/pages/plans/plans-page.component.ts @@ -0,0 +1,43 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { TeamPlansState } from '@app/features/teams/internal'; +import { ApiUrlConfig, PlanDto } from '@app/shared'; + +@Component({ + selector: 'sqx-plans-page', + styleUrls: ['./plans-page.component.scss'], + templateUrl: './plans-page.component.html', +}) +export class PlansPageComponent implements OnInit { + private overridePlanId?: string; + + public portalUrl = this.apiUrl.buildUrl('/portal/'); + + constructor( + public readonly plansState: TeamPlansState, + private readonly apiUrl: ApiUrlConfig, + private readonly route: ActivatedRoute, + ) { + } + + public ngOnInit() { + this.overridePlanId = this.route.snapshot.queryParams['planId']; + + this.plansState.load(false, this.overridePlanId); + } + + public reload() { + this.plansState.load(true, this.overridePlanId); + } + + public trackByPlan(_index: number, planInfo: { plan: PlanDto }) { + return planInfo.plan.id; + } +} diff --git a/frontend/src/app/features/teams/services/team-contributors.service.spec.ts b/frontend/src/app/features/teams/services/team-contributors.service.spec.ts new file mode 100644 index 000000000..4dc3b6655 --- /dev/null +++ b/frontend/src/app/features/teams/services/team-contributors.service.spec.ts @@ -0,0 +1,143 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; +import { TeamContributorsService } from '@app/features/teams/internal'; +import { ApiUrlConfig, ContributorDto, ContributorsDto, ContributorsPayload, Resource, ResourceLinks, Version } from '@app/shared/internal'; + +describe('TeamContributorsService', () => { + const version = new Version('1'); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], + providers: [ + TeamContributorsService, + { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, + ], + }); + }); + + afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { + httpMock.verify(); + })); + + it('should make get request to get team contributors', + inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => { + let contributors: ContributorsDto; + + contributorsService.getContributors('my-team').subscribe(result => { + contributors = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team/contributors'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush(contributorsResponse(1, 2, 3), { + headers: { + etag: '2', + }, + }); + + expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new Version('2') }); + })); + + it('should make post request to assign contributor', + inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => { + const dto = { contributorId: '123', role: 'Owner' }; + + let contributors: ContributorsDto; + + contributorsService.postContributor('my-team', dto, version).subscribe(result => { + contributors = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team/contributors'); + + expect(req.request.method).toEqual('POST'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush(contributorsResponse(1, 2, 3), { + headers: { + etag: '2', + }, + }); + + expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new Version('2') }); + })); + + it('should make delete request to remove contributor', + inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => { + const resource: Resource = { + _links: { + delete: { method: 'DELETE', href: '/api/teams/my-team/contributors/123' }, + }, + }; + + let contributors: ContributorsDto; + + contributorsService.deleteContributor('my-team', resource, version).subscribe(result => { + contributors = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team/contributors/123'); + + expect(req.request.method).toEqual('DELETE'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush(contributorsResponse(1, 2, 3), { + headers: { + etag: '2', + }, + }); + + expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new Version('2') }); + })); + + function contributorsResponse(...ids: number[]) { + return { + items: ids.map(id => ({ + contributorId: `id${id}`, + contributorName: `name${id}`, + contributorEmail: `mail${id}@squidex.io`, + role: id % 2 === 0 ? 'Owner' : 'Developer', + _links: { + update: { method: 'PUT', href: `/contributors/id${id}` }, + }, + })), + maxContributors: ids.length * 13, + _links: { + create: { method: 'POST', href: '/contributors' }, + }, + _meta: { + isInvited: 'true', + }, + }; + } +}); + +export function createContributors(...ids: ReadonlyArray): ContributorsPayload { + return { + maxContributors: ids.length * 13, + items: ids.map(createContributor), + isInvited: false, + canCreate: true, + }; +} + +export function createContributor(id: number) { + const links: ResourceLinks = { + update: { method: 'PUT', href: `/contributors/id${id}` }, + }; + + return new ContributorDto(links, `id${id}`, `name${id}`, `mail${id}@squidex.io`, id % 2 === 0 ? 'Owner' : 'Developer'); +} diff --git a/frontend/src/app/features/teams/services/team-contributors.service.ts b/frontend/src/app/features/teams/services/team-contributors.service.ts new file mode 100644 index 000000000..4b93d351f --- /dev/null +++ b/frontend/src/app/features/teams/services/team-contributors.service.ts @@ -0,0 +1,52 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ApiUrlConfig, AssignContributorDto, ContributorsDto, HTTP, mapVersioned, parseContributors, pretifyError, Resource, Version } from '@app/shared'; + +@Injectable() +export class TeamContributorsService { + constructor( + private readonly http: HttpClient, + private readonly apiUrl: ApiUrlConfig, + ) { + } + + public getContributors(teamId: string): Observable { + const url = this.apiUrl.buildUrl(`api/teams/${teamId}/contributors`); + + return HTTP.getVersioned(this.http, url).pipe( + mapVersioned(({ body }) => { + return parseContributors(body); + }), + pretifyError('i18n:contributors.loadFailed')); + } + + public postContributor(teamId: string, dto: AssignContributorDto, version: Version): Observable { + const url = this.apiUrl.buildUrl(`api/teams/${teamId}/contributors`); + + return HTTP.postVersioned(this.http, url, dto, version).pipe( + mapVersioned(({ body }) => { + return parseContributors(body); + }), + pretifyError('i18n:contributors.addFailed')); + } + + public deleteContributor(teamId: string, resource: Resource, version: Version): Observable { + const link = resource._links['delete']; + + const url = this.apiUrl.buildUrl(link.href); + + return HTTP.requestVersioned(this.http, link.method, url, version).pipe( + mapVersioned(({ body }) => { + return parseContributors(body); + }), + pretifyError('i18n:contributors.deleteFailed')); + } +} \ No newline at end of file diff --git a/frontend/src/app/features/teams/services/team-plans.service.spec.ts b/frontend/src/app/features/teams/services/team-plans.service.spec.ts new file mode 100644 index 000000000..29ee0834f --- /dev/null +++ b/frontend/src/app/features/teams/services/team-plans.service.spec.ts @@ -0,0 +1,128 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; +import { TeamPlansService } from '@app/features/teams/internal'; +import { ApiUrlConfig, PlanChangedDto, PlanDto, PlansDto, Version } from '@app/shared'; + +describe('TeamPlansService', () => { + const version = new Version('1'); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], + providers: [ + TeamPlansService, + { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, + ], + }); + }); + + afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { + httpMock.verify(); + })); + + it('should make get request to get team plans', + inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => { + let plans: PlansDto; + + plansService.getPlans('my-team').subscribe(result => { + plans = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team/plans'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush({ + teamId: 'my-team', + currentPlanId: '123', + planOwner: '456', + plans: [ + { + id: 'free', + name: 'Free', + costs: '14 €', + confirmText: 'Change for 14 € per month?', + yearlyId: 'free_yearly', + yearlyCosts: '120 €', + yearlyConfirmText: 'Change for 120 € per year?', + maxApiBytes: 128, + maxApiCalls: 1000, + maxAssetSize: 1500, + maxContributors: 2500, + }, + { + id: 'professional', + name: 'Professional', + costs: '18 €', + confirmText: 'Change for 18 € per month?', + yearlyId: 'professional_yearly', + yearlyCosts: '160 €', + yearlyConfirmText: 'Change for 160 € per year?', + maxApiBytes: 512, + maxApiCalls: 4000, + maxAssetSize: 5500, + maxContributors: 6500, + }, + ], + hasPortal: true, + }, { + headers: { + etag: '2', + }, + }); + + expect(plans!).toEqual({ + payload: { + teamId: 'my-team', + currentPlanId: '123', + planOwner: '456', + plans: [ + new PlanDto( + 'free', 'Free', '14 €', + 'Change for 14 € per month?', + 'free_yearly', '120 €', + 'Change for 120 € per year?', + 128, 1000, 1500, 2500), + new PlanDto( + 'professional', 'Professional', '18 €', + 'Change for 18 € per month?', + 'professional_yearly', '160 €', + 'Change for 160 € per year?', + 512, 4000, 5500, 6500), + ], + hasPortal: true, + }, + version: new Version('2'), + }); + })); + + it('should make put request to change plan', + inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => { + const dto = { planId: 'enterprise' }; + + let planChanged: PlanChangedDto; + + plansService.putPlan('my-team', dto, version).subscribe(result => { + planChanged = result.payload; + }); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team/plan'); + + req.flush({ redirectUri: 'http://url' }); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toBe(version.value); + + expect(planChanged!).toEqual({ redirectUri: 'http://url' }); + })); +}); diff --git a/frontend/src/app/features/teams/services/team-plans.service.ts b/frontend/src/app/features/teams/services/team-plans.service.ts new file mode 100644 index 000000000..440207d52 --- /dev/null +++ b/frontend/src/app/features/teams/services/team-plans.service.ts @@ -0,0 +1,40 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ApiUrlConfig, ChangePlanDto, HTTP, mapVersioned, parsePlans, PlanChangedDto, PlansDto, pretifyError, Version, Versioned } from '@app/shared'; + +@Injectable() +export class TeamPlansService { + constructor( + private readonly http: HttpClient, + private readonly apiUrl: ApiUrlConfig, + ) { + } + + public getPlans(teamId: string): Observable { + const url = this.apiUrl.buildUrl(`api/teams/${teamId}/plans`); + + return HTTP.getVersioned(this.http, url).pipe( + mapVersioned(({ body }) => { + return parsePlans(body); + }), + pretifyError('i18n:plans.loadFailed')); + } + + public putPlan(teamId: string, dto: ChangePlanDto, version: Version): Observable> { + const url = this.apiUrl.buildUrl(`api/teams/${teamId}/plan`); + + return HTTP.putVersioned(this.http, url, dto, version).pipe( + mapVersioned(({ body }) => { + return body; + }), + pretifyError('i18n:plans.changeFailed')); + } +} \ No newline at end of file diff --git a/frontend/src/app/features/teams/shared/settings-area.component.html b/frontend/src/app/features/teams/shared/settings-area.component.html new file mode 100644 index 000000000..3b01726e5 --- /dev/null +++ b/frontend/src/app/features/teams/shared/settings-area.component.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/app/features/dashboard/pages/cards/api-performance-card.component.scss b/frontend/src/app/features/teams/shared/settings-area.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/api-performance-card.component.scss rename to frontend/src/app/features/teams/shared/settings-area.component.scss diff --git a/frontend/src/app/features/teams/shared/settings-area.component.ts b/frontend/src/app/features/teams/shared/settings-area.component.ts new file mode 100644 index 000000000..9e4fab90e --- /dev/null +++ b/frontend/src/app/features/teams/shared/settings-area.component.ts @@ -0,0 +1,23 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component } from '@angular/core'; +import { defined, TeamsState } from '@app/shared'; + +@Component({ + selector: 'sqx-settings-area', + styleUrls: ['./settings-area.component.scss'], + templateUrl: './settings-area.component.html', +}) +export class SettingsAreaComponent { + public selectedTeam = this.teamsState.selectedTeam.pipe(defined()); + + constructor( + private readonly teamsState: TeamsState, + ) { + } +} diff --git a/frontend/src/app/features/teams/shared/settings-menu.component.html b/frontend/src/app/features/teams/shared/settings-menu.component.html new file mode 100644 index 000000000..672e35f0c --- /dev/null +++ b/frontend/src/app/features/teams/shared/settings-menu.component.html @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/frontend/src/app/features/teams/shared/settings-menu.component.scss b/frontend/src/app/features/teams/shared/settings-menu.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/features/teams/shared/settings-menu.component.ts b/frontend/src/app/features/teams/shared/settings-menu.component.ts new file mode 100644 index 000000000..ed2255ab9 --- /dev/null +++ b/frontend/src/app/features/teams/shared/settings-menu.component.ts @@ -0,0 +1,20 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TeamDto } from '@app/shared'; + +@Component({ + selector: 'sqx-settings-menu[team]', + styleUrls: ['./settings-menu.component.scss'], + templateUrl: './settings-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SettingsMenuComponent { + @Input() + public team!: TeamDto; +} diff --git a/frontend/src/app/features/teams/state/team-contributors.forms.ts b/frontend/src/app/features/teams/state/team-contributors.forms.ts new file mode 100644 index 000000000..0607ba080 --- /dev/null +++ b/frontend/src/app/features/teams/state/team-contributors.forms.ts @@ -0,0 +1,85 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { FormControl, Validators } from '@angular/forms'; +import { debounceTime, map, shareReplay } from 'rxjs/operators'; +import { AssignContributorDto, ExtendedFormGroup, Form, hasNoValue$, Types, UserDto, value$ } from '@app/shared'; + +export class AssignTeamContributorForm extends Form { + public get user() { + return this.form.controls['user']; + } + + public hasNoUser = hasNoValue$(this.user); + + constructor() { + super(new ExtendedFormGroup({ + user: new FormControl('', + Validators.required, + ), + })); + } + + protected transformSubmit(value: any) { + let contributorId = value.user; + + if (Types.is(contributorId, UserDto)) { + contributorId = contributorId.id; + } + + return { contributorId, role: 'Owner', invite: true }; + } +} + +type ImportContributorsFormType = ReadonlyArray; + +export class ImportContributorsForm extends Form { + public get import() { + return this.form.controls['import']; + } + + public numberOfEmails = value$(this.import).pipe(debounceTime(100), map(v => extractEmails(v).length), shareReplay(1)); + + public hasNoUser = this.numberOfEmails.pipe(map(v => v === 0)); + + constructor() { + super(new ExtendedFormGroup({ + import: new FormControl('', + Validators.required, + ), + })); + } + + protected transformSubmit(value: any) { + return extractEmails(value.import); + } +} + +function extractEmails(value: string) { + const result: AssignContributorDto[] = []; + + if (value) { + const added: { [email: string]: boolean } = {}; + + const emails = value.match(EMAIL_REGEX); + + if (emails) { + for (const match of emails) { + if (!added[match]) { + result.push({ contributorId: match, role: 'Owner', invite: true }); + + added[match] = true; + } + } + } + } + + return result; +} + +// eslint-disable-next-line no-useless-escape +const EMAIL_REGEX = /(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*/gim; diff --git a/frontend/src/app/features/teams/state/team-contributors.state.spec.ts b/frontend/src/app/features/teams/state/team-contributors.state.spec.ts new file mode 100644 index 000000000..41d5a1af3 --- /dev/null +++ b/frontend/src/app/features/teams/state/team-contributors.state.spec.ts @@ -0,0 +1,206 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { EMPTY, of, throwError } from 'rxjs'; +import { catchError, onErrorResumeNext } from 'rxjs/operators'; +import { IMock, It, Mock, Times } from 'typemoq'; +import { TeamContributorsService, TeamContributorsState } from '@app/features/teams/internal'; +import { ContributorDto, ContributorsPayload, DialogService, ErrorDto, versioned } from '@app/shared'; +import { createContributors } from '@app/shared/services/contributors.service.spec'; +import { TestValues } from '@app/shared/state/_test-helpers'; + +describe('TeamContributorsState', () => { + const { + team, + teamsState, + newVersion, + version, + } = TestValues; + + const allIds: number[] = []; + + for (let i = 1; i <= 20; i++) { + allIds.push(i); + } + + const oldContributors = createContributors(...allIds); + + let dialogs: IMock; + let contributorsService: IMock; + let contributorsState: TeamContributorsState; + + beforeEach(() => { + dialogs = Mock.ofType(); + + contributorsService = Mock.ofType(); + contributorsService.setup(x => x.getContributors(team)) + .returns(() => of(versioned(version, oldContributors))).verifiable(Times.atLeastOnce()); + + contributorsState = new TeamContributorsState(teamsState.object, contributorsService.object, dialogs.object); + }); + + afterEach(() => { + contributorsService.verifyAll(); + }); + + describe('Loading', () => { + it('should load contributors', () => { + contributorsState.load().subscribe(); + + expect(contributorsState.snapshot.contributors).toEqual(oldContributors.items); + expect(contributorsState.snapshot.isLoaded).toBeTruthy(); + expect(contributorsState.snapshot.isLoading).toBeFalsy(); + expect(contributorsState.snapshot.total).toEqual(20); + expect(contributorsState.snapshot.maxContributors).toBe(oldContributors.maxContributors); + expect(contributorsState.snapshot.version).toEqual(version); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); + + it('should reset loading state if loading failed', () => { + contributorsService.setup(x => x.getContributors(team)) + .returns(() => throwError(() => 'Service Error')); + + contributorsState.load().pipe(onErrorResumeNext()).subscribe(); + + expect(contributorsState.snapshot.isLoading).toBeFalsy(); + }); + + it('should not load if already loaded', () => { + contributorsState.load(true).subscribe(); + contributorsState.loadIfNotLoaded().subscribe(); + + expect().nothing(); + }); + + it('should only show current page of contributors', () => { + contributorsState.load().subscribe(); + + let contributors: ReadonlyArray; + + contributorsState.contributorsFiltered.subscribe(result => { + contributors = result; + }); + + expect(contributors!).toEqual(oldContributors.items.slice(0, 10)); + expect(contributorsState.snapshot.page).toEqual(0); + expect(contributorsState.snapshot.pageSize).toEqual(10); + }); + + it('should show with new pagination if paging', () => { + contributorsState.load().subscribe(); + contributorsState.page({ page: 1, pageSize: 10 }); + + let contributors: ReadonlyArray; + + contributorsState.contributorsFiltered.subscribe(result => { + contributors = result; + }); + + expect(contributors!).toEqual(oldContributors.items.slice(10, 20)); + expect(contributorsState.snapshot.page).toEqual(1); + expect(contributorsState.snapshot.pageSize).toEqual(10); + }); + + it('should show filtered contributors if searching', () => { + contributorsState.load().subscribe(); + contributorsState.search('4'); + + let contributors: ReadonlyArray; + + contributorsState.contributorsFiltered.subscribe(result => { + contributors = result; + }); + + expect(contributors!).toEqual(createContributors(4, 14).items); + expect(contributorsState.snapshot.page).toEqual(0); + expect(contributorsState.snapshot.pageSize).toEqual(10); + }); + + it('should show notification on load if reload is true', () => { + contributorsState.load(true).subscribe(); + + expect().nothing(); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); + }); + + describe('Updates', () => { + beforeEach(() => { + contributorsState.load().subscribe(); + }); + + it('should update contributors if user assigned', () => { + const updated = createContributors(5, 6); + + const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' }; + + contributorsService.setup(x => x.postContributor(team, request, version)) + .returns(() => of(versioned(newVersion, updated))).verifiable(); + + contributorsState.assign(request).subscribe(); + + expectNewContributors(updated); + }); + + it('should return proper error if user to add does not exist', () => { + const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' }; + + contributorsService.setup(x => x.postContributor(team, request, version)) + .returns(() => throwError(() => new ErrorDto(404, '404'))); + + let error: ErrorDto; + + contributorsState.assign(request).pipe( + catchError(err => { + error = err; + + return EMPTY; + }), + ).subscribe(); + + expect(error!.message).toBe('i18n:contributors.userNotFound'); + }); + + it('should return original error if not a 404', () => { + const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' }; + + contributorsService.setup(x => x.postContributor(team, request, version)) + .returns(() => throwError(() => new ErrorDto(500, '500'))); + + let error: ErrorDto; + + contributorsState.assign(request).pipe( + catchError(err => { + error = err; + + return EMPTY; + }), + ).subscribe(); + + expect(error!.message).toBe('500'); + }); + + it('should update contributors if contribution revoked', () => { + const updated = createContributors(5, 6); + + contributorsService.setup(x => x.deleteContributor(team, oldContributors.items[0], version)) + .returns(() => of(versioned(newVersion, updated))).verifiable(); + + contributorsState.revoke(oldContributors.items[0]).subscribe(); + + expectNewContributors(updated); + }); + + function expectNewContributors(updated: ContributorsPayload) { + expect(contributorsState.snapshot.contributors).toEqual(updated.items); + expect(contributorsState.snapshot.maxContributors).toBe(updated.maxContributors); + expect(contributorsState.snapshot.version).toEqual(newVersion); + } + }); +}); diff --git a/frontend/src/app/features/teams/state/team-contributors.state.ts b/frontend/src/app/features/teams/state/team-contributors.state.ts new file mode 100644 index 000000000..fae2c4c67 --- /dev/null +++ b/frontend/src/app/features/teams/state/team-contributors.state.ts @@ -0,0 +1,173 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Injectable } from '@angular/core'; +import { EMPTY, Observable, throwError } from 'rxjs'; +import { catchError, finalize, tap } from 'rxjs/operators'; +import { TeamContributorsService } from '@app/features/teams/internal'; +import { AssignContributorDto, ContributorDto, ContributorsPayload, DialogService, ErrorDto, getPagingInfo, ListState, shareMapSubscribed, shareSubscribed, State, TeamsState, Types, Version } from '@app/shared'; + +interface Snapshot extends ListState { + // The current contributors. + contributors: ReadonlyArray; + + // The maximum allowed users. + maxContributors: number; + + // The team version. + version: Version; + + // Indicates if the user can add a contributor. + canCreate?: boolean; +} + +@Injectable() +export class TeamContributorsState extends State { + public contributors = + this.project(x => x.contributors); + + public paging = + this.project(x => getPagingInfo(x, x.contributors.length)); + + public query = + this.project(x => x.query); + + public queryRegex = + this.projectFrom(this.query, x => getRegex(x)); + + public maxContributors = + this.project(x => x.maxContributors); + + public isLoaded = + this.project(x => x.isLoaded === true); + + public isLoading = + this.project(x => x.isLoading === true); + + public canCreate = + this.project(x => x.canCreate === true); + + public contributorsFiltered = + this.project(getFilteredContributors); + + public get teamId() { + return this.teamsState.teamId; + } + + constructor( + private readonly teamsState: TeamsState, + private readonly contributorsService: TeamContributorsService, + private readonly dialogs: DialogService, + ) { + super({ + contributors: [], + maxContributors: -1, + page: 0, + pageSize: 10, + total: 0, + version: Version.EMPTY, + }, 'Contributors'); + } + + public loadIfNotLoaded(): Observable { + if (this.snapshot.isLoaded) { + return EMPTY; + } + + return this.loadInternal(false); + } + + public load(isReload = false, update: Partial = {}): Observable { + if (!isReload) { + this.resetState(update, 'Loading Initial'); + } + + return this.loadInternal(isReload); + } + + private loadInternal(isReload: boolean): Observable { + this.next({ isLoading: true }, 'Loading Started'); + + return this.contributorsService.getContributors(this.teamId).pipe( + tap(({ version, payload }) => { + if (isReload) { + this.dialogs.notifyInfo('i18n:contributors.reloaded'); + } + + this.replaceContributors(version, payload); + }), + finalize(() => { + this.next({ isLoading: false }, 'Loading Done'); + }), + shareSubscribed(this.dialogs)); + } + + public page(paging: { page: number; pageSize: number }) { + this.next(paging, 'Results Paged'); + } + + public search(query: string) { + this.next({ query }, 'Results Filtered'); + } + + public revoke(contributor: ContributorDto): Observable { + return this.contributorsService.deleteContributor(this.teamId, contributor, this.version).pipe( + tap(({ version, payload }) => { + this.replaceContributors(version, payload); + }), + shareSubscribed(this.dialogs)); + } + + public assign(request: AssignContributorDto, options?: { silent: boolean }): Observable { + return this.contributorsService.postContributor(this.teamId, request, this.version).pipe( + catchError(error => { + if (Types.is(error, ErrorDto) && error.statusCode === 404) { + return throwError(() => new ErrorDto(404, 'i18n:contributors.userNotFound')); + } else { + return throwError(() => error); + } + }), + tap(({ version, payload }) => { + this.replaceContributors(version, payload); + }), + shareMapSubscribed(this.dialogs, x => x.payload.isInvited, options)); + } + + private replaceContributors(version: Version, { canCreate, items, maxContributors }: ContributorsPayload) { + this.next({ + canCreate, + contributors: items, + isLoaded: true, + isLoading: false, + maxContributors, + total: items.length, + version, + }, 'Loading Success / Updated'); + } + + private get version() { + return this.snapshot.version; + } +} + +function getRegex(query?: string): RegExp | undefined { + return query ? new RegExp(query, 'i') : undefined; +} + +function getFilteredContributors(snapshot: Snapshot) { + const { contributors, query, page, pageSize } = snapshot; + + let filtered = contributors; + + if (query) { + const regex = new RegExp(query, 'i'); + + filtered = filtered.filter(x => regex.test(x.contributorName)); + } + + return filtered.slice(page * pageSize, (page + 1) * pageSize); +} diff --git a/frontend/src/app/features/teams/state/team-plans.state.spec.ts b/frontend/src/app/features/teams/state/team-plans.state.spec.ts new file mode 100644 index 000000000..7d00a6871 --- /dev/null +++ b/frontend/src/app/features/teams/state/team-plans.state.spec.ts @@ -0,0 +1,151 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { of, throwError } from 'rxjs'; +import { onErrorResumeNext } from 'rxjs/operators'; +import { IMock, It, Mock, Times } from 'typemoq'; +import { TeamPlansService, TeamPlansState } from '@app/features/teams/internal'; +import { DialogService, PlanDto, versioned } from '@app/shared'; +import { TestValues } from '@app/shared/state/_test-helpers'; + +describe('TeamPlansState', () => { + const { + authService, + creator, + newVersion, + team, + teamsState, + version, + } = TestValues; + + const oldPlans = { + currentPlanId: 'id1', + planOwner: creator, + plans: [ + new PlanDto('id1', 'name1', '100€', undefined, 'id1_yearly', '200€', undefined, 1, 1, 1, 1), + new PlanDto('id2', 'name2', '400€', undefined, 'id2_yearly', '800€', undefined, 2, 2, 2, 2), + ], + hasPortal: true, + }; + + let dialogs: IMock; + let plansService: IMock; + let plansState: TeamPlansState; + + beforeEach(() => { + dialogs = Mock.ofType(); + + plansService = Mock.ofType(); + plansState = new TeamPlansState(teamsState.object, authService.object, dialogs.object, plansService.object); + }); + + afterEach(() => { + plansService.verifyAll(); + }); + + describe('Loading', () => { + it('should load plans', () => { + plansService.setup(x => x.getPlans(team)) + .returns(() => of(versioned(version, oldPlans))).verifiable(); + + plansState.load().subscribe(); + + expect(plansState.snapshot.plans).toEqual([ + { isSelected: true, isYearlySelected: false, plan: oldPlans.plans[0] }, + { isSelected: false, isYearlySelected: false, plan: oldPlans.plans[1] }, + ]); + expect(plansState.snapshot.isOwner).toBeFalsy(); + expect(plansState.snapshot.isLoaded).toBeTruthy(); + expect(plansState.snapshot.hasPortal).toBeTruthy(); + expect(plansState.snapshot.version).toEqual(version); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); + + it('should load plans with overriden id', () => { + plansService.setup(x => x.getPlans(team)) + .returns(() => of(versioned(version, oldPlans))).verifiable(); + + plansState.load(false, 'id2_yearly').subscribe(); + + expect(plansState.snapshot.plans).toEqual([ + { isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] }, + { isSelected: false, isYearlySelected: true, plan: oldPlans.plans[1] }, + ]); + expect(plansState.snapshot.hasPortal).toBeTruthy(); + expect(plansState.snapshot.isLoaded).toBeTruthy(); + expect(plansState.snapshot.isLoading).toBeFalsy(); + expect(plansState.snapshot.isOwner).toBeFalsy(); + expect(plansState.snapshot.version).toEqual(version); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); + + it('should reset loading state if loading failed', () => { + plansService.setup(x => x.getPlans(team)) + .returns(() => throwError(() => 'Service Error')); + + plansState.load().pipe(onErrorResumeNext()).subscribe(); + + expect(plansState.snapshot.isLoading).toBeFalsy(); + }); + + it('should show notification on load if reload is true', () => { + plansService.setup(x => x.getPlans(team)) + .returns(() => of(versioned(version, oldPlans))).verifiable(); + + plansState.load(true).subscribe(); + + expect().nothing(); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); + }); + + describe('Updates', () => { + beforeEach(() => { + plansState.window = { location: {} }; + + plansService.setup(x => x.getPlans(team)) + .returns(() => of(versioned(version, oldPlans))).verifiable(); + + plansState.load().subscribe(); + }); + + it('should redirect if returning url', () => { + plansState.window = { location: {} }; + + const result = { redirectUri: 'http://url' }; + + plansService.setup(x => x.putPlan(team, It.isAny(), version)) + .returns(() => of(versioned(newVersion, result))); + + plansState.change('free').pipe(onErrorResumeNext()).subscribe(); + + expect(plansState.snapshot.plans).toEqual([ + { isSelected: true, isYearlySelected: false, plan: oldPlans.plans[0] }, + { isSelected: false, isYearlySelected: false, plan: oldPlans.plans[1] }, + ]); + expect(plansState.window.location.href).toBe(result.redirectUri); + expect(plansState.snapshot.version).toEqual(version); + }); + + it('should update plans if no returning url', () => { + plansService.setup(x => x.putPlan(team, It.isAny(), version)) + .returns(() => of(versioned(newVersion, { redirectUri: '' }))); + + plansState.change('id2_yearly').pipe(onErrorResumeNext()).subscribe(); + + expect(plansState.snapshot.plans).toEqual([ + { isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] }, + { isSelected: false, isYearlySelected: true, plan: oldPlans.plans[1] }, + ]); + expect(plansState.snapshot.isOwner).toBeTruthy(); + expect(plansState.snapshot.version).toEqual(newVersion); + }); + }); +}); diff --git a/frontend/src/app/features/teams/state/team-plans.state.ts b/frontend/src/app/features/teams/state/team-plans.state.ts new file mode 100644 index 000000000..f1156daaf --- /dev/null +++ b/frontend/src/app/features/teams/state/team-plans.state.ts @@ -0,0 +1,147 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { finalize, tap } from 'rxjs/operators'; +import { TeamPlansService } from '@app/features/teams/internal'; +import { AuthService, DialogService, LoadingState, PlanDto, shareSubscribed, State, TeamsState, Version } from '@app/shared'; + +export interface PlanInfo { + // The plan. + plan: PlanDto; + + // Indicates if the yearly subscription is selected. + isYearlySelected?: boolean; + + // Indicates if the monthly subscription is selected. + isSelected?: boolean; +} + +interface Snapshot extends LoadingState { + // The current plans. + plans: ReadonlyArray; + + // Indicates if the user is the plan owner. + isOwner?: boolean; + + // The user, who owns the plan. + planOwner?: string; + + // Indicates if there is a billing portal for the current Squidex instance. + hasPortal?: boolean; + + // The team version. + version: Version; +} + +@Injectable() +export class TeamPlansState extends State { + public plans = + this.project(x => x.plans); + + public planOwner = + this.project(x => x.planOwner); + + public isOwner = + this.project(x => x.isOwner === true); + + public isLoaded = + this.project(x => x.isLoaded === true); + + public isLoading = + this.project(x => x.isLoading === true); + + public isDisabled = + this.project(x => !x.isOwner); + + public hasPortal = + this.project(x => x.hasPortal); + + public get teamId() { + return this.teamsState.teamId; + } + + public window = window; + + constructor( + private readonly teamsState: TeamsState, + private readonly authState: AuthService, + private readonly dialogs: DialogService, + private readonly plansService: TeamPlansService, + ) { + super({ plans: [], version: Version.EMPTY }, 'Plans'); + } + + public load(isReload = false, overridePlanId?: string): Observable { + if (!isReload) { + this.resetState('Loading Initial'); + } + + return this.loadInternal(isReload, overridePlanId); + } + + private loadInternal(isReload: boolean, overridePlanId?: string): Observable { + this.next({ isLoading: true }, 'Loading Started'); + + return this.plansService.getPlans(this.teamId).pipe( + tap(({ version, payload }) => { + if (isReload) { + this.dialogs.notifyInfo('i18n:plans.reloaded'); + } + + const planId = overridePlanId || payload.currentPlanId; + const plans = payload.plans.map(x => createPlan(x, planId)); + + this.next({ + hasPortal: payload.hasPortal, + isLoaded: true, + isLoading: false, + isOwner: !payload.planOwner || payload.planOwner === this.userId, + planOwner: payload.planOwner, + plans, + version, + }, 'Loading Success'); + }), + finalize(() => { + this.next({ isLoading: false }, 'Loading Done'); + }), + shareSubscribed(this.dialogs)); + } + + public change(planId: string): Observable { + return this.plansService.putPlan(this.teamId, { planId }, this.version).pipe( + tap(({ payload, version }) => { + if (payload.redirectUri && payload.redirectUri.length > 0) { + this.window.location.href = payload.redirectUri; + } else { + this.next(s => { + const plans = s.plans.map(x => createPlan(x.plan, planId)); + + return { ...s, plans, isOwner: true, version }; + }, 'Change'); + } + }), + shareSubscribed(this.dialogs)); + } + + private get userId() { + return this.authState.user!.id; + } + + private get version() { + return this.snapshot.version; + } +} + +function createPlan(plan: PlanDto, id: string) { + return { + plan, + isSelected: plan.id === id, + isYearlySelected: plan.yearlyId === id, + }; +} diff --git a/frontend/src/app/features/teams/team-area.component.html b/frontend/src/app/features/teams/team-area.component.html new file mode 100644 index 000000000..c58546298 --- /dev/null +++ b/frontend/src/app/features/teams/team-area.component.html @@ -0,0 +1,11 @@ + + + + + +
+ +
+
\ No newline at end of file diff --git a/frontend/src/app/features/teams/team-area.component.scss b/frontend/src/app/features/teams/team-area.component.scss new file mode 100644 index 000000000..b8f08c432 --- /dev/null +++ b/frontend/src/app/features/teams/team-area.component.scss @@ -0,0 +1,12 @@ +@import 'mixins'; +@import 'vars'; + +.nav-link { + padding-bottom: .6rem; + padding-top: .6rem; + position: relative; +} + +.icon-angle-right { + @include absolute(14px, 1.5rem); +} \ No newline at end of file diff --git a/frontend/src/app/features/teams/team-area.component.ts b/frontend/src/app/features/teams/team-area.component.ts new file mode 100644 index 000000000..57cc5f428 --- /dev/null +++ b/frontend/src/app/features/teams/team-area.component.ts @@ -0,0 +1,23 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component } from '@angular/core'; +import { defined, TeamsState } from '@app/shared'; + +@Component({ + selector: 'sqx-team-area', + styleUrls: ['./team-area.component.scss'], + templateUrl: './team-area.component.html', +}) +export class TeamAreaComponent { + public selectedTeam = this.teamsState.selectedTeam.pipe(defined()); + + constructor( + private readonly teamsState: TeamsState, + ) { + } +} diff --git a/frontend/src/app/framework/state.ts b/frontend/src/app/framework/state.ts index b0935bc7b..f1f8a7d31 100644 --- a/frontend/src/app/framework/state.ts +++ b/frontend/src/app/framework/state.ts @@ -63,7 +63,15 @@ export function getPagingInfo(state: ListState, count: number) { return { page, pageSize, total, count }; } -export interface ListState { +export interface LoadingState { + // True if currently loading. + isLoading?: boolean; + + // True if already loaded. + isLoaded?: boolean; +} + +export interface ListState extends LoadingState { // The total number of items. total: number; diff --git a/frontend/src/app/shared/components/assets/asset-folder-dropdown.state.ts b/frontend/src/app/shared/components/assets/asset-folder-dropdown.state.ts index 640cb9f69..59c3ffc6c 100644 --- a/frontend/src/app/shared/components/assets/asset-folder-dropdown.state.ts +++ b/frontend/src/app/shared/components/assets/asset-folder-dropdown.state.ts @@ -5,9 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AssetPathItem } from '@app/shared/internal'; +import { AssetPathItem, LoadingState } from '@app/shared/internal'; -export interface AssetFolderDropdowNode { +export interface AssetFolderDropdowNode extends LoadingState { // The child folders. children: AssetFolderDropdowNode[]; @@ -17,12 +17,6 @@ export interface AssetFolderDropdowNode { // True if selected. isSelected?: boolean; - // True if loading - isLoading?: boolean; - - // True if loaded - isLoaded?: boolean; - // True if expanded isExpanded?: boolean; diff --git a/frontend/src/app/features/dashboard/pages/cards/api-calls-card.component.html b/frontend/src/app/shared/components/cards/api-calls-card.component.html similarity index 76% rename from frontend/src/app/features/dashboard/pages/cards/api-calls-card.component.html rename to frontend/src/app/shared/components/cards/api-calls-card.component.html index 4b7f0522c..77d53d66a 100644 --- a/frontend/src/app/features/dashboard/pages/cards/api-calls-card.component.html +++ b/frontend/src/app/shared/components/cards/api-calls-card.component.html @@ -2,8 +2,8 @@
{{ 'dashboard.apiCallsCard' | sqxTranslate }} -
- + diff --git a/frontend/src/app/features/dashboard/pages/cards/api-traffic-card.component.scss b/frontend/src/app/shared/components/cards/api-calls-card.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/api-traffic-card.component.scss rename to frontend/src/app/shared/components/cards/api-calls-card.component.scss diff --git a/frontend/src/app/features/dashboard/pages/cards/api-calls-card.component.ts b/frontend/src/app/shared/components/cards/api-calls-card.component.ts similarity index 83% rename from frontend/src/app/features/dashboard/pages/cards/api-calls-card.component.ts rename to frontend/src/app/shared/components/cards/api-calls-card.component.ts index 6124cf1f5..6104209df 100644 --- a/frontend/src/app/features/dashboard/pages/cards/api-calls-card.component.ts +++ b/frontend/src/app/shared/components/cards/api-calls-card.component.ts @@ -6,18 +6,17 @@ */ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; -import { AppDto, CallsUsageDto, UsagesService } from '@app/shared'; -import { ChartHelpers, ChartOptions } from './shared'; +import { AppDto, CallsUsageDto, ChartHelpers, ChartOptions, UsagesService } from '@app/shared/internal'; @Component({ - selector: 'sqx-api-calls-card[app][usage]', + selector: 'sqx-api-calls-card[usage]', styleUrls: ['./api-calls-card.component.scss'], templateUrl: './api-calls-card.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ApiCallsCardComponent implements OnChanges { @Input() - public app!: AppDto; + public app: AppDto | undefined | null; @Input() public usage?: CallsUsageDto; @@ -48,8 +47,8 @@ export class ApiCallsCardComponent implements OnChanges { } } - public downloadLog() { - this.usagesService.getLog(this.app.name) + public downloadLog(app: AppDto) { + this.usagesService.getLog(app.name) .subscribe(url => { window.open(url, '_blank'); }); diff --git a/frontend/src/app/features/dashboard/pages/cards/api-calls-summary-card.component.html b/frontend/src/app/shared/components/cards/api-calls-summary-card.component.html similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/api-calls-summary-card.component.html rename to frontend/src/app/shared/components/cards/api-calls-summary-card.component.html diff --git a/frontend/src/app/features/dashboard/pages/cards/api-traffic-summary-card.component.scss b/frontend/src/app/shared/components/cards/api-calls-summary-card.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/api-traffic-summary-card.component.scss rename to frontend/src/app/shared/components/cards/api-calls-summary-card.component.scss diff --git a/frontend/src/app/features/dashboard/pages/cards/api-calls-summary-card.component.ts b/frontend/src/app/shared/components/cards/api-calls-summary-card.component.ts similarity index 83% rename from frontend/src/app/features/dashboard/pages/cards/api-calls-summary-card.component.ts rename to frontend/src/app/shared/components/cards/api-calls-summary-card.component.ts index be6155f89..e840fb9b1 100644 --- a/frontend/src/app/features/dashboard/pages/cards/api-calls-summary-card.component.ts +++ b/frontend/src/app/shared/components/cards/api-calls-summary-card.component.ts @@ -6,18 +6,15 @@ */ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; -import { AppDto, CallsUsageDto } from '@app/shared'; +import { CallsUsageDto } from '@app/shared/internal'; @Component({ - selector: 'sqx-api-calls-summary-card[app][usage]', + selector: 'sqx-api-calls-summary-card[usage]', styleUrls: ['./api-calls-summary-card.component.scss'], templateUrl: './api-calls-summary-card.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ApiCallsSummaryCardComponent implements OnChanges { - @Input() - public app!: AppDto; - @Input() public usage?: CallsUsageDto; diff --git a/frontend/src/app/features/dashboard/pages/cards/api-performance-card.component.html b/frontend/src/app/shared/components/cards/api-performance-card.component.html similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/api-performance-card.component.html rename to frontend/src/app/shared/components/cards/api-performance-card.component.html diff --git a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-count-card.component.scss b/frontend/src/app/shared/components/cards/api-performance-card.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/asset-uploads-count-card.component.scss rename to frontend/src/app/shared/components/cards/api-performance-card.component.scss diff --git a/frontend/src/app/features/dashboard/pages/cards/api-performance-card.component.ts b/frontend/src/app/shared/components/cards/api-performance-card.component.ts similarity index 88% rename from frontend/src/app/features/dashboard/pages/cards/api-performance-card.component.ts rename to frontend/src/app/shared/components/cards/api-performance-card.component.ts index 4019ad66e..a86bcbbc8 100644 --- a/frontend/src/app/features/dashboard/pages/cards/api-performance-card.component.ts +++ b/frontend/src/app/shared/components/cards/api-performance-card.component.ts @@ -6,19 +6,15 @@ */ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; -import { AppDto, CallsUsageDto } from '@app/shared'; -import { ChartHelpers, ChartOptions } from './shared'; +import { CallsUsageDto, ChartHelpers, ChartOptions } from '@app/shared/internal'; @Component({ - selector: 'sqx-api-performance-card[app][usage]', + selector: 'sqx-api-performance-card[usage]', styleUrls: ['./api-performance-card.component.scss'], templateUrl: './api-performance-card.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ApiPerformanceCardComponent implements OnChanges { - @Input() - public app!: AppDto; - @Input() public usage?: CallsUsageDto; diff --git a/frontend/src/app/features/dashboard/pages/cards/api-traffic-card.component.html b/frontend/src/app/shared/components/cards/api-traffic-card.component.html similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/api-traffic-card.component.html rename to frontend/src/app/shared/components/cards/api-traffic-card.component.html diff --git a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-card.component.scss b/frontend/src/app/shared/components/cards/api-traffic-card.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-card.component.scss rename to frontend/src/app/shared/components/cards/api-traffic-card.component.scss diff --git a/frontend/src/app/features/dashboard/pages/cards/api-traffic-card.component.ts b/frontend/src/app/shared/components/cards/api-traffic-card.component.ts similarity index 89% rename from frontend/src/app/features/dashboard/pages/cards/api-traffic-card.component.ts rename to frontend/src/app/shared/components/cards/api-traffic-card.component.ts index 0f67830cb..6d69b264a 100644 --- a/frontend/src/app/features/dashboard/pages/cards/api-traffic-card.component.ts +++ b/frontend/src/app/shared/components/cards/api-traffic-card.component.ts @@ -6,19 +6,15 @@ */ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; -import { AppDto, CallsUsageDto } from '@app/shared'; -import { ChartHelpers, ChartOptions } from './shared'; +import { CallsUsageDto, ChartHelpers, ChartOptions } from '@app/shared/internal'; @Component({ - selector: 'sqx-api-traffic-card[app][usage]', + selector: 'sqx-api-traffic-card[usage]', styleUrls: ['./api-traffic-card.component.scss'], templateUrl: './api-traffic-card.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ApiTrafficCardComponent implements OnChanges { - @Input() - public app!: AppDto; - @Input() public usage?: CallsUsageDto; diff --git a/frontend/src/app/features/dashboard/pages/cards/api-traffic-summary-card.component.html b/frontend/src/app/shared/components/cards/api-traffic-summary-card.component.html similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/api-traffic-summary-card.component.html rename to frontend/src/app/shared/components/cards/api-traffic-summary-card.component.html diff --git a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.scss b/frontend/src/app/shared/components/cards/api-traffic-summary-card.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.scss rename to frontend/src/app/shared/components/cards/api-traffic-summary-card.component.scss diff --git a/frontend/src/app/features/dashboard/pages/cards/api-traffic-summary-card.component.ts b/frontend/src/app/shared/components/cards/api-traffic-summary-card.component.ts similarity index 83% rename from frontend/src/app/features/dashboard/pages/cards/api-traffic-summary-card.component.ts rename to frontend/src/app/shared/components/cards/api-traffic-summary-card.component.ts index f7aaa9643..aa9918c22 100644 --- a/frontend/src/app/features/dashboard/pages/cards/api-traffic-summary-card.component.ts +++ b/frontend/src/app/shared/components/cards/api-traffic-summary-card.component.ts @@ -6,18 +6,15 @@ */ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; -import { AppDto, CallsUsageDto } from '@app/shared'; +import { CallsUsageDto } from '@app/shared/internal'; @Component({ - selector: 'sqx-api-traffic-summary-card[app][usage]', + selector: 'sqx-api-traffic-summary-card[usage]', styleUrls: ['./api-traffic-summary-card.component.scss'], templateUrl: './api-traffic-summary-card.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ApiTrafficSummaryCardComponent implements OnChanges { - @Input() - public app!: AppDto; - @Input() public usage?: CallsUsageDto; diff --git a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-count-card.component.html b/frontend/src/app/shared/components/cards/asset-uploads-count-card.component.html similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/asset-uploads-count-card.component.html rename to frontend/src/app/shared/components/cards/asset-uploads-count-card.component.html diff --git a/frontend/src/app/features/dashboard/pages/cards/support-card.component.scss b/frontend/src/app/shared/components/cards/asset-uploads-count-card.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/support-card.component.scss rename to frontend/src/app/shared/components/cards/asset-uploads-count-card.component.scss diff --git a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-count-card.component.ts b/frontend/src/app/shared/components/cards/asset-uploads-count-card.component.ts similarity index 85% rename from frontend/src/app/features/dashboard/pages/cards/asset-uploads-count-card.component.ts rename to frontend/src/app/shared/components/cards/asset-uploads-count-card.component.ts index ce578e520..4f4a635b9 100644 --- a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-count-card.component.ts +++ b/frontend/src/app/shared/components/cards/asset-uploads-count-card.component.ts @@ -6,19 +6,15 @@ */ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; -import { AppDto, StorageUsagePerDateDto } from '@app/shared'; -import { ChartHelpers, ChartOptions } from './shared'; +import { ChartHelpers, ChartOptions, StorageUsagePerDateDto } from '@app/shared/internal'; @Component({ - selector: 'sqx-asset-uploads-count-card[app][usage]', + selector: 'sqx-asset-uploads-count-card[usage]', styleUrls: ['./asset-uploads-count-card.component.scss'], templateUrl: './asset-uploads-count-card.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AssetUploadsCountCardComponent implements OnChanges { - @Input() - public app!: AppDto; - @Input() public usage?: ReadonlyArray; diff --git a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-card.component.html b/frontend/src/app/shared/components/cards/asset-uploads-size-card.component.html similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-card.component.html rename to frontend/src/app/shared/components/cards/asset-uploads-size-card.component.html diff --git a/frontend/src/app/shared/components/cards/asset-uploads-size-card.component.scss b/frontend/src/app/shared/components/cards/asset-uploads-size-card.component.scss new file mode 100644 index 000000000..2742d895e --- /dev/null +++ b/frontend/src/app/shared/components/cards/asset-uploads-size-card.component.scss @@ -0,0 +1,2 @@ +@import 'mixins'; +@import 'vars'; \ No newline at end of file diff --git a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-card.component.ts b/frontend/src/app/shared/components/cards/asset-uploads-size-card.component.ts similarity index 86% rename from frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-card.component.ts rename to frontend/src/app/shared/components/cards/asset-uploads-size-card.component.ts index d79d7c8a3..4021ef7e3 100644 --- a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-card.component.ts +++ b/frontend/src/app/shared/components/cards/asset-uploads-size-card.component.ts @@ -6,19 +6,15 @@ */ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; -import { AppDto, StorageUsagePerDateDto } from '@app/shared'; -import { ChartHelpers, ChartOptions } from './shared'; +import { ChartHelpers, ChartOptions, StorageUsagePerDateDto } from '@app/shared/internal'; @Component({ - selector: 'sqx-asset-uploads-size-card[app][usage]', + selector: 'sqx-asset-uploads-size-card[usage]', styleUrls: ['./asset-uploads-size-card.component.scss'], templateUrl: './asset-uploads-size-card.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AssetUploadsSizeCardComponent implements OnChanges { - @Input() - public app!: AppDto; - @Input() public usage?: ReadonlyArray; diff --git a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.html b/frontend/src/app/shared/components/cards/asset-uploads-size-summary-card.component.html similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.html rename to frontend/src/app/shared/components/cards/asset-uploads-size-summary-card.component.html diff --git a/frontend/src/app/shared/components/cards/asset-uploads-size-summary-card.component.scss b/frontend/src/app/shared/components/cards/asset-uploads-size-summary-card.component.scss new file mode 100644 index 000000000..2742d895e --- /dev/null +++ b/frontend/src/app/shared/components/cards/asset-uploads-size-summary-card.component.scss @@ -0,0 +1,2 @@ +@import 'mixins'; +@import 'vars'; \ No newline at end of file diff --git a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.ts b/frontend/src/app/shared/components/cards/asset-uploads-size-summary-card.component.ts similarity index 82% rename from frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.ts rename to frontend/src/app/shared/components/cards/asset-uploads-size-summary-card.component.ts index 2bff7bae3..30f5eecd8 100644 --- a/frontend/src/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.ts +++ b/frontend/src/app/shared/components/cards/asset-uploads-size-summary-card.component.ts @@ -6,18 +6,15 @@ */ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; -import { AppDto, CurrentStorageDto } from '@app/shared'; +import { CurrentStorageDto } from '@app/shared/internal'; @Component({ - selector: 'sqx-asset-uploads-size-summary-card[app][usage]', + selector: 'sqx-asset-uploads-size-summary-card[usage]', styleUrls: ['./asset-uploads-size-summary-card.component.scss'], templateUrl: './asset-uploads-size-summary-card.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AssetUploadsSizeSummaryCardComponent implements OnChanges { - @Input() - public app!: AppDto; - @Input() public usage?: CurrentStorageDto; diff --git a/frontend/src/app/features/dashboard/pages/cards/iframe-card.component.html b/frontend/src/app/shared/components/cards/iframe-card.component.html similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/iframe-card.component.html rename to frontend/src/app/shared/components/cards/iframe-card.component.html diff --git a/frontend/src/app/features/dashboard/pages/cards/iframe-card.component.scss b/frontend/src/app/shared/components/cards/iframe-card.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/iframe-card.component.scss rename to frontend/src/app/shared/components/cards/iframe-card.component.scss diff --git a/frontend/src/app/features/dashboard/pages/cards/iframe-card.component.ts b/frontend/src/app/shared/components/cards/iframe-card.component.ts similarity index 85% rename from frontend/src/app/features/dashboard/pages/cards/iframe-card.component.ts rename to frontend/src/app/shared/components/cards/iframe-card.component.ts index 62049eb3b..c76e37800 100644 --- a/frontend/src/app/features/dashboard/pages/cards/iframe-card.component.ts +++ b/frontend/src/app/shared/components/cards/iframe-card.component.ts @@ -6,18 +6,14 @@ */ import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild } from '@angular/core'; -import { AppDto } from '@app/shared'; @Component({ - selector: 'sqx-iframe-card[app]', + selector: 'sqx-iframe-card', styleUrls: ['./iframe-card.component.scss'], templateUrl: './iframe-card.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class IFrameCardComponent implements AfterViewInit { - @Input() - public app!: AppDto; - @Input() public options: any; diff --git a/frontend/src/app/features/dashboard/pages/cards/random-cat-card.component.html b/frontend/src/app/shared/components/cards/random-cat-card.component.html similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/random-cat-card.component.html rename to frontend/src/app/shared/components/cards/random-cat-card.component.html diff --git a/frontend/src/app/features/dashboard/pages/cards/random-cat-card.component.scss b/frontend/src/app/shared/components/cards/random-cat-card.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/random-cat-card.component.scss rename to frontend/src/app/shared/components/cards/random-cat-card.component.scss diff --git a/frontend/src/app/features/dashboard/pages/cards/random-cat-card.component.ts b/frontend/src/app/shared/components/cards/random-cat-card.component.ts similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/random-cat-card.component.ts rename to frontend/src/app/shared/components/cards/random-cat-card.component.ts diff --git a/frontend/src/app/features/dashboard/pages/cards/random-dog-card.component.html b/frontend/src/app/shared/components/cards/random-dog-card.component.html similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/random-dog-card.component.html rename to frontend/src/app/shared/components/cards/random-dog-card.component.html diff --git a/frontend/src/app/features/dashboard/pages/cards/random-dog-card.component.scss b/frontend/src/app/shared/components/cards/random-dog-card.component.scss similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/random-dog-card.component.scss rename to frontend/src/app/shared/components/cards/random-dog-card.component.scss diff --git a/frontend/src/app/features/dashboard/pages/cards/random-dog-card.component.ts b/frontend/src/app/shared/components/cards/random-dog-card.component.ts similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/random-dog-card.component.ts rename to frontend/src/app/shared/components/cards/random-dog-card.component.ts diff --git a/frontend/src/app/features/dashboard/pages/cards/shared.ts b/frontend/src/app/shared/components/cards/shared.ts similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/shared.ts rename to frontend/src/app/shared/components/cards/shared.ts diff --git a/frontend/src/app/features/dashboard/pages/cards/support-card.component.html b/frontend/src/app/shared/components/cards/support-card.component.html similarity index 100% rename from frontend/src/app/features/dashboard/pages/cards/support-card.component.html rename to frontend/src/app/shared/components/cards/support-card.component.html diff --git a/frontend/src/app/shared/components/cards/support-card.component.scss b/frontend/src/app/shared/components/cards/support-card.component.scss new file mode 100644 index 000000000..2742d895e --- /dev/null +++ b/frontend/src/app/shared/components/cards/support-card.component.scss @@ -0,0 +1,2 @@ +@import 'mixins'; +@import 'vars'; \ No newline at end of file diff --git a/frontend/src/app/shared/components/cards/support-card.component.ts b/frontend/src/app/shared/components/cards/support-card.component.ts new file mode 100644 index 000000000..bec7a352a --- /dev/null +++ b/frontend/src/app/shared/components/cards/support-card.component.ts @@ -0,0 +1,17 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'sqx-support-card', + styleUrls: ['./support-card.component.scss'], + templateUrl: './support-card.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SupportCardComponent { +} diff --git a/frontend/src/app/shared/components/history/history.component.ts b/frontend/src/app/shared/components/history/history.component.ts index 21882011a..b2a94cae8 100644 --- a/frontend/src/app/shared/components/history/history.component.ts +++ b/frontend/src/app/shared/components/history/history.component.ts @@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { merge, Observable, timer } from 'rxjs'; import { delay } from 'rxjs/operators'; -import { allParams, AppsState, HistoryChannelUpdated, HistoryEventDto, HistoryService, MessageBus, switchSafe } from '@app/shared/internal'; +import { allParams, AppsState, HistoryChannelUpdated, HistoryEventDto, HistoryService, MessageBus, switchSafe, TeamsState } from '@app/shared/internal'; @Component({ selector: 'sqx-history', @@ -25,16 +25,25 @@ export class HistoryComponent { timer(0, 10000), this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000)), ).pipe( - switchSafe(() => this.historyService.getHistory(this.appsState.appName, this.channel))); + switchSafe(() => this.getHistory())); constructor( private readonly appsState: AppsState, private readonly historyService: HistoryService, private readonly messageBus: MessageBus, private readonly route: ActivatedRoute, + private readonly teamsState: TeamsState, ) { } + private getHistory() { + if (this.teamsState.teamId) { + return this.historyService.getHistoryForTeam(this.teamsState.teamId, this.channel); + } else { + return this.historyService.getHistory(this.appsState.appName, this.channel); + } + } + private calculateChannel(): string { let channel = this.route.snapshot.data.channel; diff --git a/frontend/src/app/shared/components/team-form.component.html b/frontend/src/app/shared/components/team-form.component.html new file mode 100644 index 000000000..4a75077e4 --- /dev/null +++ b/frontend/src/app/shared/components/team-form.component.html @@ -0,0 +1,39 @@ +
+ + + {{ 'teams.create' | sqxTranslate }} + + + + + +
+ + + + + + + + {{ 'teams.teamNameHint' | sqxTranslate }} + +
+ +
+ + {{ 'teams.teamNameWarning' | sqxTranslate }} + +
+
+ + + + + + +
+
\ No newline at end of file diff --git a/frontend/src/app/shared/components/team-form.component.scss b/frontend/src/app/shared/components/team-form.component.scss new file mode 100644 index 000000000..2742d895e --- /dev/null +++ b/frontend/src/app/shared/components/team-form.component.scss @@ -0,0 +1,2 @@ +@import 'mixins'; +@import 'vars'; \ No newline at end of file diff --git a/frontend/src/app/shared/components/team-form.component.ts b/frontend/src/app/shared/components/team-form.component.ts new file mode 100644 index 000000000..1166188b2 --- /dev/null +++ b/frontend/src/app/shared/components/team-form.component.ts @@ -0,0 +1,48 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core'; +import { ApiUrlConfig, CreateTeamForm, TeamsState } from '@app/shared/internal'; + +@Component({ + selector: 'sqx-team-form', + styleUrls: ['./team-form.component.scss'], + templateUrl: './team-form.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TeamFormComponent { + @Output() + public complete = new EventEmitter(); + + public createForm = new CreateTeamForm(); + + constructor( + public readonly apiUrl: ApiUrlConfig, + private readonly teamsStore: TeamsState, + ) { + } + + public emitComplete() { + this.complete.emit(); + } + + public createTeam() { + const value = this.createForm.submit(); + + if (value) { + this.teamsStore.create(value) + .subscribe({ + next: () => { + this.emitComplete(); + }, + error: error => { + this.createForm.submitFailed(error); + }, + }); + } + } +} diff --git a/frontend/src/app/shared/declarations.ts b/frontend/src/app/shared/declarations.ts index 3c60073e7..0935f675d 100644 --- a/frontend/src/app/shared/declarations.ts +++ b/frontend/src/app/shared/declarations.ts @@ -8,8 +8,8 @@ export * from './components/app-form.component'; export * from './components/assets/asset-dialog.component'; export * from './components/assets/asset-folder-dialog.component'; -export * from './components/assets/asset-folder-dropdown.component'; export * from './components/assets/asset-folder-dropdown-item.component'; +export * from './components/assets/asset-folder-dropdown.component'; export * from './components/assets/asset-folder.component'; export * from './components/assets/asset-history.component'; export * from './components/assets/asset-path.component'; @@ -21,6 +21,18 @@ export * from './components/assets/assets-selector.component'; export * from './components/assets/image-cropper.component'; export * from './components/assets/image-focus-point.component'; export * from './components/assets/pipes'; +export * from './components/cards/api-calls-card.component'; +export * from './components/cards/api-calls-summary-card.component'; +export * from './components/cards/api-performance-card.component'; +export * from './components/cards/api-traffic-card.component'; +export * from './components/cards/api-traffic-summary-card.component'; +export * from './components/cards/asset-uploads-count-card.component'; +export * from './components/cards/asset-uploads-size-card.component'; +export * from './components/cards/asset-uploads-size-summary-card.component'; +export * from './components/cards/iframe-card.component'; +export * from './components/cards/random-cat-card.component'; +export * from './components/cards/random-dog-card.component'; +export * from './components/cards/support-card.component'; export * from './components/comments/comment.component'; export * from './components/comments/comments.component'; export * from './components/contents/content-list-cell.directive'; @@ -55,17 +67,21 @@ export * from './components/search/query-list.component'; export * from './components/search/search-form.component'; export * from './components/search/shared-queries.component'; export * from './components/table-header.component'; +export * from './components/team-form.component'; export * from './components/watching-users.component'; export * from './guards/app-must-exist.guard'; export * from './guards/content-must-exist.guard'; export * from './guards/load-apps.guard'; export * from './guards/load-languages.guard'; export * from './guards/load-schemas.guard'; +export * from './guards/load-teams.guard'; export * from './guards/must-be-authenticated.guard'; export * from './guards/must-be-not-authenticated.guard'; export * from './guards/rule-must-exist.guard'; export * from './guards/schema-must-exist-published.guard'; export * from './guards/schema-must-exist.guard'; export * from './guards/schema-must-not-be-singleton.guard'; +export * from './guards/team-must-exist.guard'; export * from './guards/unset-app.guard'; +export * from './guards/unset-team.guard'; export * from './internal'; diff --git a/frontend/src/app/shared/guards/load-teams.guard.spec.ts b/frontend/src/app/shared/guards/load-teams.guard.spec.ts new file mode 100644 index 000000000..b0303b98f --- /dev/null +++ b/frontend/src/app/shared/guards/load-teams.guard.spec.ts @@ -0,0 +1,32 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { firstValueFrom, of } from 'rxjs'; +import { IMock, Mock, Times } from 'typemoq'; +import { TeamsState } from '@app/shared/internal'; +import { LoadTeamsGuard } from './load-teams.guard'; + +describe('LoadTeamsGuard', () => { + let teamsState: IMock; + let teamGuard: LoadTeamsGuard; + + beforeEach(() => { + teamsState = Mock.ofType(); + teamGuard = new LoadTeamsGuard(teamsState.object); + }); + + it('should load teams', async () => { + teamsState.setup(x => x.load()) + .returns(() => of(null)); + + const result = await firstValueFrom(teamGuard.canActivate()); + + expect(result).toBeTruthy(); + + teamsState.verify(x => x.load(), Times.once()); + }); +}); diff --git a/frontend/src/app/shared/guards/load-teams.guard.ts b/frontend/src/app/shared/guards/load-teams.guard.ts new file mode 100644 index 000000000..1a8fa3f42 --- /dev/null +++ b/frontend/src/app/shared/guards/load-teams.guard.ts @@ -0,0 +1,24 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Injectable } from '@angular/core'; +import { CanActivate } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { TeamsState } from './../state/teams.state'; + +@Injectable() +export class LoadTeamsGuard implements CanActivate { + constructor( + private readonly teamsState: TeamsState, + ) { + } + + public canActivate(): Observable { + return this.teamsState.load().pipe(map(() => true)); + } +} diff --git a/frontend/src/app/shared/guards/team-must-exist.guard.spec.ts b/frontend/src/app/shared/guards/team-must-exist.guard.spec.ts new file mode 100644 index 000000000..d07dc929b --- /dev/null +++ b/frontend/src/app/shared/guards/team-must-exist.guard.spec.ts @@ -0,0 +1,50 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Router } from '@angular/router'; +import { firstValueFrom, of } from 'rxjs'; +import { IMock, Mock, Times } from 'typemoq'; +import { TeamsState } from '@app/shared/internal'; +import { TeamMustExistGuard } from './team-must-exist.guard'; + +describe('TeamMustExistGuard', () => { + const route: any = { + params: { + teamName: 'my-team', + }, + }; + + let router: IMock; + let teamsState: IMock; + let teamGuard: TeamMustExistGuard; + + beforeEach(() => { + router = Mock.ofType(); + teamsState = Mock.ofType(); + teamGuard = new TeamMustExistGuard(teamsState.object, router.object); + }); + + it('should navigate to 404 page if team is not found', async () => { + teamsState.setup(x => x.select('my-team')) + .returns(() => of(null)); + + const result = await firstValueFrom(teamGuard.canActivate(route)); + + expect(result).toBeFalsy(); + + router.verify(x => x.navigate(['/404']), Times.once()); + }); + + it('should return true if team is found', async () => { + teamsState.setup(x => x.select('my-team')) + .returns(() => of({})); + + const result = await firstValueFrom(teamGuard.canActivate(route)); + + expect(result).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/shared/guards/team-must-exist.guard.ts b/frontend/src/app/shared/guards/team-must-exist.guard.ts new file mode 100644 index 000000000..ab059caae --- /dev/null +++ b/frontend/src/app/shared/guards/team-must-exist.guard.ts @@ -0,0 +1,36 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; +import { TeamsState } from './../state/teams.state'; + +@Injectable() +export class TeamMustExistGuard implements CanActivate { + constructor( + private readonly teamsState: TeamsState, + private readonly router: Router, + ) { + } + + public canActivate(route: ActivatedRouteSnapshot): Observable { + const teamName = route.params['teamName']; + + const result = + this.teamsState.select(teamName).pipe( + tap(team => { + if (!team) { + this.router.navigate(['/404']); + } + }), + map(team => !!team)); + + return result; + } +} diff --git a/frontend/src/app/shared/guards/unset-team.guard.spec.ts b/frontend/src/app/shared/guards/unset-team.guard.spec.ts new file mode 100644 index 000000000..9522b6434 --- /dev/null +++ b/frontend/src/app/shared/guards/unset-team.guard.spec.ts @@ -0,0 +1,32 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { firstValueFrom, of } from 'rxjs'; +import { IMock, Mock, Times } from 'typemoq'; +import { TeamsState } from '@app/shared/internal'; +import { UnsetTeamGuard } from './unset-team.guard'; + +describe('UnsetTeamGuard', () => { + let teamsState: IMock; + let teamGuard: UnsetTeamGuard; + + beforeEach(() => { + teamsState = Mock.ofType(); + teamGuard = new UnsetTeamGuard(teamsState.object); + }); + + it('should unselect team', async () => { + teamsState.setup(x => x.select(null)) + .returns(() => of(null)); + + const result = await firstValueFrom(teamGuard.canActivate()); + + expect(result).toBeTruthy(); + + teamsState.verify(x => x.select(null), Times.once()); + }); +}); diff --git a/frontend/src/app/shared/guards/unset-team.guard.ts b/frontend/src/app/shared/guards/unset-team.guard.ts new file mode 100644 index 000000000..8d4eb20d4 --- /dev/null +++ b/frontend/src/app/shared/guards/unset-team.guard.ts @@ -0,0 +1,24 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Injectable } from '@angular/core'; +import { CanActivate } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { TeamsState } from './../state/teams.state'; + +@Injectable() +export class UnsetTeamGuard implements CanActivate { + constructor( + private readonly teamsState: TeamsState, + ) { + } + + public canActivate(): Observable { + return this.teamsState.select(null).pipe(map(a => a === null)); + } +} diff --git a/frontend/src/app/shared/internal.ts b/frontend/src/app/shared/internal.ts index bcaa8ea96..1a734c664 100644 --- a/frontend/src/app/shared/internal.ts +++ b/frontend/src/app/shared/internal.ts @@ -6,6 +6,7 @@ */ export * from '@app/framework'; +export * from './components/cards/shared'; export * from './interceptors/auth.interceptor'; export * from './services/app-languages.service'; export * from './services/apps.service'; @@ -28,7 +29,9 @@ export * from './services/rules.service'; export * from './services/schemas.service'; export * from './services/schemas.types'; export * from './services/search.service'; +export * from './services/shared'; export * from './services/stock-photo.service'; +export * from './services/teams.service'; export * from './services/templates.service'; export * from './services/templates.service'; export * from './services/translations.service'; @@ -71,6 +74,8 @@ export * from './state/schemas.forms'; export * from './state/schemas.state'; export * from './state/settings'; export * from './state/table-settings'; +export * from './state/teams.forms'; +export * from './state/teams.state'; export * from './state/templates.state'; export * from './state/ui-languages'; export * from './state/ui.state'; diff --git a/frontend/src/app/shared/module.ts b/frontend/src/app/shared/module.ts index eb308d152..f5a85a815 100644 --- a/frontend/src/app/shared/module.ts +++ b/frontend/src/app/shared/module.ts @@ -10,12 +10,14 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { ModuleWithProviders, NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { MentionModule } from 'angular-mentions'; +import { ChartModule } from 'angular2-chartjs'; import { NgxDocViewerModule } from 'ngx-doc-viewer'; import { SqxFrameworkModule } from '@app/framework'; -import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetScriptsState, AssetsListComponent, AssetsSelectorComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentListCellDirective, ContentListCellResizeDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthDirective, ContentMustExistGuard, ContentsColumnsPipe, ContentSelectorComponent, ContentSelectorItemComponent, ContentsService, ContentsState, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, FilterOperatorPipe, GeolocationEditorComponent, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, ReferenceInputComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, TableHeaderComponent, TemplatesService, TemplatesState, TranslationsService, TranslationStatusComponent, UIService, UIState, UnsetAppGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WatchingUsersComponent, WorkflowsService, WorkflowsState } from './declarations'; +import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetScriptsState, AssetsListComponent, AssetsSelectorComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentListCellDirective, ContentListCellResizeDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthDirective, ContentMustExistGuard, ContentsColumnsPipe, ContentSelectorComponent, ContentSelectorItemComponent, ContentsService, ContentsState, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, FilterOperatorPipe, GeolocationEditorComponent, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, IFrameCardComponent, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, LoadTeamsGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, RandomCatCardComponent, RandomDogCardComponent, ReferenceInputComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, SupportCardComponent, TableHeaderComponent, TeamFormComponent, TeamMustExistGuard, TeamsService, TeamsState, TemplatesService, TemplatesState, TranslationsService, TranslationStatusComponent, UIService, UIState, UnsetAppGuard, UnsetTeamGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WatchingUsersComponent, WorkflowsService, WorkflowsState } from './declarations'; @NgModule({ imports: [ + ChartModule, DragDropModule, MentionModule, NgxDocViewerModule, @@ -23,6 +25,11 @@ import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, SqxFrameworkModule, ], declarations: [ + ApiCallsCardComponent, + ApiCallsSummaryCardComponent, + ApiPerformanceCardComponent, + ApiTrafficCardComponent, + ApiTrafficSummaryCardComponent, AppFormComponent, AssetComponent, AssetDialogComponent, @@ -37,6 +44,9 @@ import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AssetsSelectorComponent, AssetTextEditorComponent, AssetUploaderComponent, + AssetUploadsCountCardComponent, + AssetUploadsSizeCardComponent, + AssetUploadsSizeSummaryCardComponent, AssetUrlPipe, CommentComponent, CommentsComponent, @@ -62,6 +72,7 @@ import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, + IFrameCardComponent, ImageCropperComponent, ImageFocusPointComponent, MarkdownEditorComponent, @@ -70,13 +81,17 @@ import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, QueryComponent, QueryListComponent, QueryPathComponent, + RandomCatCardComponent, + RandomDogCardComponent, ReferenceInputComponent, RichEditorComponent, SavedQueriesComponent, SchemaCategoryComponent, SearchFormComponent, SortingComponent, + SupportCardComponent, TableHeaderComponent, + TeamFormComponent, TranslationStatusComponent, UserDtoPicture, UserIdPicturePipe, @@ -87,6 +102,11 @@ import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, WatchingUsersComponent, ], exports: [ + ApiCallsCardComponent, + ApiCallsSummaryCardComponent, + ApiPerformanceCardComponent, + ApiTrafficCardComponent, + ApiTrafficSummaryCardComponent, AppFormComponent, AssetComponent, AssetDialogComponent, @@ -98,6 +118,9 @@ import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AssetsListComponent, AssetsSelectorComponent, AssetUploaderComponent, + AssetUploadsCountCardComponent, + AssetUploadsSizeCardComponent, + AssetUploadsSizeSummaryCardComponent, AssetUrlPipe, CommentComponent, CommentsComponent, @@ -120,17 +143,22 @@ import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, + IFrameCardComponent, MarkdownEditorComponent, NotifoComponent, PreviewableType, QueryListComponent, + RandomCatCardComponent, + RandomDogCardComponent, ReferenceInputComponent, RichEditorComponent, RouterModule, SavedQueriesComponent, SchemaCategoryComponent, SearchFormComponent, + SupportCardComponent, TableHeaderComponent, + TeamFormComponent, TranslationStatusComponent, UserDtoPicture, UserIdPicturePipe, @@ -173,6 +201,7 @@ export class SqxSharedModule { LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, + LoadTeamsGuard, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, @@ -193,12 +222,16 @@ export class SqxSharedModule { SchemaTagSource, SearchService, StockPhotoService, + TeamMustExistGuard, TemplatesService, TemplatesState, + TeamsState, + TeamsService, TranslationsService, UIService, UIState, UnsetAppGuard, + UnsetTeamGuard, UsagesService, UsersProviderService, UsersService, diff --git a/frontend/src/app/shared/services/apps.service.spec.ts b/frontend/src/app/shared/services/apps.service.spec.ts index a22c55350..759c0bd07 100644 --- a/frontend/src/app/shared/services/apps.service.spec.ts +++ b/frontend/src/app/shared/services/apps.service.spec.ts @@ -7,8 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, AppDto, AppsService, DateTime, ErrorDto, Resource, ResourceLinks, Version } from '@app/shared/internal'; -import { AppSettingsDto, AssetScriptsDto, AssetScriptsPayload, EditorDto, PatternDto } from './apps.service'; +import { ApiUrlConfig, AppDto, AppSettingsDto, AppsService, AssetScriptsDto, AssetScriptsPayload, DateTime, EditorDto, ErrorDto, PatternDto, Resource, ResourceLinks, Version } from '@app/shared/internal'; describe('AppsService', () => { const version = new Version('1'); @@ -50,6 +49,28 @@ describe('AppsService', () => { expect(apps!).toEqual([createApp(12), createApp(13)]); })); + + it('should make get request to get team apps', + inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { + let apps: ReadonlyArray; + + appsService.getTeamApps('my-team').subscribe(result => { + apps = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team/apps'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush([ + appResponse(12), + appResponse(13), + ]); + + expect(apps!).toEqual([createApp(12), createApp(13)]); + })); + it('should make get request to get app', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { let app: AppDto; @@ -204,6 +225,30 @@ describe('AppsService', () => { expect(app!).toEqual(createApp(12)); })); + it('should make put request to transfer app', + inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { + const resource: Resource = { + _links: { + transfer: { method: 'PUT', href: '/api/apps/my-app/team' }, + }, + }; + + let app: AppDto; + + appsService.transferApp('my-app', resource, { teamId: 'my-team' }, version).subscribe(result => { + app = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/team'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toBe(version.value); + + req.flush(appResponse(12)); + + expect(app!).toEqual(createApp(12)); + })); + it('should make post request to upload app image', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { const resource: Resource = { @@ -282,11 +327,11 @@ describe('AppsService', () => { inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { const resource: Resource = { _links: { - delete: { method: 'DELETE', href: '/api/apps/my-app/contributors/me' }, + leave: { method: 'DELETE', href: '/api/apps/my-app/contributors/me' }, }, }; - appsService.deleteApp('my-app', resource).subscribe(); + appsService.leaveApp('my-app', resource).subscribe(); const req = httpMock.expectOne('http://service/p/api/apps/my-app/contributors/me'); @@ -332,6 +377,7 @@ describe('AppsService', () => { permissions: ['Owner'], roleName: `Role${id}`, roleProperties: createProperties(id), + teamId: `app-team${key}`, _links: { update: { method: 'PUT', href: `apps/${id}` }, }, @@ -391,7 +437,8 @@ export function createApp(id: number, suffix = '') { id % 2 === 0, id % 2 === 0, `Role${id}`, - createProperties(id)); + createProperties(id), + `app-team${key}`); } export function createAppSettings(id: number, suffix = '') { diff --git a/frontend/src/app/shared/services/apps.service.ts b/frontend/src/app/shared/services/apps.service.ts index 92d365848..dc9f4b19d 100644 --- a/frontend/src/app/shared/services/apps.service.ts +++ b/frontend/src/app/shared/services/apps.service.ts @@ -28,6 +28,7 @@ export class AppDto { public readonly canReadRules: boolean; public readonly canReadSchemas: boolean; public readonly canReadWorkflows: boolean; + public readonly canUpdateTeam: boolean; public readonly canUpdateGeneral: boolean; public readonly canUpdateImage: boolean; public readonly canUploadAssets: boolean; @@ -51,9 +52,12 @@ export class AppDto { public readonly canAccessContent: boolean, public readonly roleName: string | undefined, public readonly roleProperties: {}, + public readonly teamId: string | null, ) { this._links = links; + this.displayName = StringHelper.firstNonEmpty(this.label, this.name); + this.canCreateSchema = hasAnyLink(links, 'schemas/create'); this.canDelete = hasAnyLink(links, 'delete'); this.canReadAssets = hasAnyLink(links, 'assets'); @@ -68,13 +72,11 @@ export class AppDto { this.canReadRules = hasAnyLink(links, 'rules'); this.canReadSchemas = hasAnyLink(links, 'schemas'); this.canReadWorkflows = hasAnyLink(links, 'workflows'); + this.canUpdateTeam = hasAnyLink(links, 'transfer'); this.canUpdateGeneral = hasAnyLink(links, 'update'); this.canUpdateImage = hasAnyLink(links, 'image/upload'); this.canUploadAssets = hasAnyLink(links, 'assets/create'); - this.image = getLinkUrl(links, 'image'); - - this.displayName = StringHelper.firstNonEmpty(this.label, this.name); } } @@ -140,6 +142,11 @@ export type CreateAppDto = Readonly<{ name: string; }>; +export type TransferToTeamDto = Readonly<{ + // The target team ID. + teamId: string | null; +}>; + export type UpdateAppDto = Readonly<{ // The label, which is like a display name. label?: string; @@ -168,6 +175,18 @@ export class AppsService { pretifyError('i18n:apps.loadFailed')); } + public getTeamApps(teamId: string): Observable> { + const url = this.apiUrl.buildUrl(`/api/teams/${teamId}/apps`); + + return this.http.get(url).pipe( + map(body => { + const apps = body.map(parseApp); + + return apps; + }), + pretifyError('i18n:apps.loadFailed')); + } + public getApp(appName: string): Observable { const url = this.apiUrl.buildUrl(`/api/apps/${appName}`); @@ -202,8 +221,20 @@ export class AppsService { pretifyError('i18n:apps.updateFailed')); } - public getSettings(name: string): Observable { - const url = this.apiUrl.buildUrl(`/api/apps/${name}/settings`); + public transferApp(appName: string, resource: Resource, dto: TransferToTeamDto, version: Version): Observable { + const link = resource._links['transfer']; + + const url = this.apiUrl.buildUrl(link.href); + + return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + map(({ payload }) => { + return parseApp(payload.body); + }), + pretifyError('i18n:apps.transferFailed')); + } + + public getSettings(appName: string): Observable { + const url = this.apiUrl.buildUrl(`/api/apps/${appName}/settings`); return this.http.get(url).pipe( map(body => { @@ -224,8 +255,8 @@ export class AppsService { pretifyError('i18n:apps.updateSettingsFailed')); } - public getAssetScripts(name: string): Observable { - const url = this.apiUrl.buildUrl(`/api/apps/${name}/assets/scripts`); + public getAssetScripts(appName: string): Observable { + const url = this.apiUrl.buildUrl(`/api/apps/${appName}/assets/scripts`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { @@ -320,7 +351,8 @@ function parseApp(response: any & Resource) { response.canAccessApi, response.canAccessContent, response.roleName, - response.roleProperties); + response.roleProperties, + response.teamId); } function parseAppSettings(response: any & Resource) { diff --git a/frontend/src/app/shared/services/contributors.service.ts b/frontend/src/app/shared/services/contributors.service.ts index ac081d37b..c2d0aa663 100644 --- a/frontend/src/app/shared/services/contributors.service.ts +++ b/frontend/src/app/shared/services/contributors.service.ts @@ -8,57 +8,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; - -export class ContributorDto { - public readonly _links: ResourceLinks; - - public readonly canRevoke: boolean; - public readonly canUpdate: boolean; - - public get token() { - return `subject:${this.contributorId}`; - } - - constructor(links: ResourceLinks, - public readonly contributorId: string, - public readonly contributorName: string, - public readonly contributorEmail: string, - public readonly role: string, - ) { - this._links = links; - - this.canRevoke = hasAnyLink(links, 'delete'); - this.canUpdate = hasAnyLink(links, 'update'); - } -} - -export type ContributorsDto = Versioned; - -export type ContributorsPayload = Readonly<{ - // The list of contributors. - items: ReadonlyArray; - - // The number of allowed contributors. - maxContributors: number; - - // True, if the user has been invited. - isInvited?: boolean; - - // True, if the user has permission to create a contributor. - canCreate?: boolean; -}>; - -export type AssignContributorDto = Readonly<{ - // The user ID. - contributorId: string; - - // The role for the contributor. - role: string; - - // True, if the user should be invited. - invite?: boolean; -}>; +import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Resource, Version } from '@app/framework'; +import { AssignContributorDto, ContributorsDto, parseContributors } from './shared'; +export * from './shared'; @Injectable() export class ContributorsService { @@ -99,21 +51,4 @@ export class ContributorsService { }), pretifyError('i18n:contributors.deleteFailed')); } -} - -function parseContributors(response: { items: any[]; maxContributors: number } & Resource): ContributorsPayload { - const { items: list, maxContributors, _meta, _links } = response; - const items = list.map(parseContributor); - - const canCreate = hasAnyLink(_links, 'create'); - - return { items, maxContributors, canCreate, isInvited: _meta?.['isInvited'] === '1' }; -} - -function parseContributor(response: any) { - return new ContributorDto(response._links, - response.contributorId, - response.contributorName, - response.contributorEmail, - response.role); } \ No newline at end of file diff --git a/frontend/src/app/shared/services/history.service.spec.ts b/frontend/src/app/shared/services/history.service.spec.ts index c93fcf71e..e5279c27e 100644 --- a/frontend/src/app/shared/services/history.service.spec.ts +++ b/frontend/src/app/shared/services/history.service.spec.ts @@ -39,29 +39,54 @@ describe('HistoryService', () => { expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); - req.flush([ - { - actor: 'User1', - eventId: '1', - eventType: 'Type 1', - message: 'Message 1', - version: 2, - created: '2016-12-12T10:10', - }, - { - actor: 'User2', - eventId: '2', - eventType: 'Type 2', - message: 'Message 2', - version: 3, - created: '2016-12-13T10:10', - }, - ]); - - expect(events!).toEqual( - [ - new HistoryEventDto('1', 'User1', 'Type 1', 'Message 1', DateTime.parseISO('2016-12-12T10:10Z'), new Version('2')), - new HistoryEventDto('2', 'User2', 'Type 2', 'Message 2', DateTime.parseISO('2016-12-13T10:10Z'), new Version('3')), - ]); + req.flush(historyResponse()); + + expect(events!).toEqual(createHistory()); + })); + + it('should make get request to get history events for a team', + inject([HistoryService, HttpTestingController], (historyService: HistoryService, httpMock: HttpTestingController) => { + let events: ReadonlyArray; + + historyService.getHistoryForTeam('my-team', 'settings.contributors').subscribe(result => { + events = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team/history?channel=settings.contributors'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush(historyResponse()); + + expect(events!).toEqual(createHistory()); })); }); + +export function createHistory() { + return [ + new HistoryEventDto('1', 'User1', 'Type 1', 'Message 1', DateTime.parseISO('2016-12-12T10:10Z'), new Version('2')), + new HistoryEventDto('2', 'User2', 'Type 2', 'Message 2', DateTime.parseISO('2016-12-13T10:10Z'), new Version('3')), + ]; +} + +function historyResponse() { + return [ + { + actor: 'User1', + eventId: '1', + eventType: 'Type 1', + message: 'Message 1', + version: 2, + created: '2016-12-12T10:10', + }, + { + actor: 'User2', + eventId: '2', + eventType: 'Type 2', + message: 'Message 2', + version: 3, + created: '2016-12-13T10:10', + }, + ]; +} diff --git a/frontend/src/app/shared/services/history.service.ts b/frontend/src/app/shared/services/history.service.ts index 3069ad7dd..486eb2e34 100644 --- a/frontend/src/app/shared/services/history.service.ts +++ b/frontend/src/app/shared/services/history.service.ts @@ -86,6 +86,22 @@ export class HistoryService { }), pretifyError('i18n:history.loadFailed')); } + + public getHistoryForTeam(teamId: string, channel: string): Observable> { + const url = this.apiUrl.buildUrl(`api/teams/${teamId}/history?channel=${channel}`); + + const options = { + headers: new HttpHeaders({ + 'X-Silent': '1', + }), + }; + + return this.http.get(url, options).pipe( + map(body => { + return parseHistoryEvents(body); + }), + pretifyError('i18n:history.loadFailed')); + } } function parseHistoryEvents(response: any[]) { diff --git a/frontend/src/app/shared/services/plans.service.spec.ts b/frontend/src/app/shared/services/plans.service.spec.ts index e24d62f50..2d75adfbd 100644 --- a/frontend/src/app/shared/services/plans.service.spec.ts +++ b/frontend/src/app/shared/services/plans.service.spec.ts @@ -42,6 +42,7 @@ describe('PlansService', () => { expect(req.request.headers.get('If-Match')).toBeNull(); req.flush({ + teamId: 'my-team', currentPlanId: '123', planOwner: '456', plans: [ @@ -81,6 +82,7 @@ describe('PlansService', () => { expect(plans!).toEqual({ payload: { + teamId: 'my-team', currentPlanId: '123', planOwner: '456', plans: [ diff --git a/frontend/src/app/shared/services/plans.service.ts b/frontend/src/app/shared/services/plans.service.ts index aa826a756..f04757227 100644 --- a/frontend/src/app/shared/services/plans.service.ts +++ b/frontend/src/app/shared/services/plans.service.ts @@ -9,49 +9,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Version, Versioned } from '@app/framework'; - -export class PlanDto { - constructor( - public readonly id: string, - public readonly name: string, - public readonly costs: string, - public readonly confirmText: string | undefined, - public readonly yearlyId: string, - public readonly yearlyCosts: string, - public readonly yearlyConfirmText: string | undefined, - public readonly maxApiBytes: number, - public readonly maxApiCalls: number, - public readonly maxAssetSize: number, - public readonly maxContributors: number, - ) { - } -} - -export type PlansDto = Versioned; - -export type PlansPayload = Readonly<{ - // The ID of the current plan. - currentPlanId: string; - - // The user, who owns the plan. - planOwner: string; - - // True, if the installation has a billing portal. - hasPortal: boolean; - - // The actual plans. - plans: ReadonlyArray; -}>; - -export type PlanChangedDto = Readonly<{ - // The redirect URI. - redirectUri?: string; -}>; - -export type ChangePlanDto = Readonly<{ - // The new plan ID. - planId: string; -}>; +import { ChangePlanDto, parsePlans, PlanChangedDto, PlansDto } from './shared'; +export * from './shared'; @Injectable() export class PlansService { @@ -80,26 +39,4 @@ export class PlansService { }), pretifyError('i18n:plans.changeFailed')); } -} - -function parsePlans(response: { plans: any[]; hasPortal: boolean; currentPlanId: string; planOwner: string }): PlansPayload { - const { plans: list, currentPlanId, hasPortal, planOwner } = response; - const plans = list.map(parsePlan); - - return { plans, planOwner, currentPlanId, hasPortal }; -} - -function parsePlan(response: any) { - return new PlanDto( - response.id, - response.name, - response.costs, - response.confirmText, - response.yearlyId, - response.yearlyCosts, - response.yearlyConfirmText, - response.maxApiBytes, - response.maxApiCalls, - response.maxAssetSize, - response.maxContributors); } \ No newline at end of file diff --git a/frontend/src/app/shared/services/shared.ts b/frontend/src/app/shared/services/shared.ts new file mode 100644 index 000000000..fcf14cc04 --- /dev/null +++ b/frontend/src/app/shared/services/shared.ts @@ -0,0 +1,143 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { hasAnyLink, Resource, ResourceLinks, Versioned } from '@app/framework'; + +export class ContributorDto { + public readonly _links: ResourceLinks; + + public readonly canRevoke: boolean; + public readonly canUpdate: boolean; + + public get token() { + return `subject:${this.contributorId}`; + } + + constructor(links: ResourceLinks, + public readonly contributorId: string, + public readonly contributorName: string, + public readonly contributorEmail: string, + public readonly role: string, + ) { + this._links = links; + + this.canRevoke = hasAnyLink(links, 'delete'); + this.canUpdate = hasAnyLink(links, 'update'); + } +} + +export type ContributorsDto = Versioned; + +export type ContributorsPayload = Readonly<{ + // The list of contributors. + items: ReadonlyArray; + + // The number of allowed contributors. + maxContributors: number; + + // True, if the user has been invited. + isInvited?: boolean; + + // True, if the user has permission to create a contributor. + canCreate?: boolean; +}>; + +export type AssignContributorDto = Readonly<{ + // The user ID. + contributorId: string; + + // The role for the contributor. + role: string; + + // True, if the user should be invited. + invite?: boolean; +}>; + +export class PlanDto { + constructor( + public readonly id: string, + public readonly name: string, + public readonly costs: string, + public readonly confirmText: string | undefined, + public readonly yearlyId: string, + public readonly yearlyCosts: string, + public readonly yearlyConfirmText: string | undefined, + public readonly maxApiBytes: number, + public readonly maxApiCalls: number, + public readonly maxAssetSize: number, + public readonly maxContributors: number, + ) { + } +} + +export type PlansDto = Versioned; + +export type PlansPayload = Readonly<{ + // The ID of the current plan. + currentPlanId: string; + + // The user, who owns the plan. + planOwner: string; + + // True, if the installation has a billing portal. + hasPortal: boolean; + + // The ID of the team. + teamId?: string | null; + + // The actual plans. + plans: ReadonlyArray; +}>; + +export type PlanChangedDto = Readonly<{ + // The redirect URI. + redirectUri?: string; +}>; + +export type ChangePlanDto = Readonly<{ + // The new plan ID. + planId: string; +}>; + +export function parsePlans(response: { plans: any[]; hasPortal: boolean; currentPlanId: string; planOwner: string; teamId: string | null }): PlansPayload { + const { plans: list, currentPlanId, hasPortal, planOwner, teamId } = response; + const plans = list.map(parsePlan); + + return { plans, planOwner, currentPlanId, hasPortal, teamId }; +} + +export function parsePlan(response: any) { + return new PlanDto( + response.id, + response.name, + response.costs, + response.confirmText, + response.yearlyId, + response.yearlyCosts, + response.yearlyConfirmText, + response.maxApiBytes, + response.maxApiCalls, + response.maxAssetSize, + response.maxContributors); +} + +export function parseContributors(response: { items: any[]; maxContributors: number } & Resource): ContributorsPayload { + const { items: list, maxContributors, _meta, _links } = response; + const items = list.map(parseContributor); + + const canCreate = hasAnyLink(_links, 'create'); + + return { items, maxContributors, canCreate, isInvited: _meta?.['isInvited'] === '1' }; +} + +export function parseContributor(response: any) { + return new ContributorDto(response._links, + response.contributorId, + response.contributorName, + response.contributorEmail, + response.role); +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/teams.service.spec.ts b/frontend/src/app/shared/services/teams.service.spec.ts new file mode 100644 index 000000000..0cc574bfc --- /dev/null +++ b/frontend/src/app/shared/services/teams.service.spec.ts @@ -0,0 +1,175 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; +import { ApiUrlConfig, DateTime, Resource, ResourceLinks, TeamDto, TeamsService, Version } from '@app/shared/internal'; + +describe('TeamsService', () => { + const version = new Version('1'); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], + providers: [ + TeamsService, + { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, + ], + }); + }); + + afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { + httpMock.verify(); + })); + + it('should make get request to get teams', + inject([TeamsService, HttpTestingController], (teamsService: TeamsService, httpMock: HttpTestingController) => { + let teams: ReadonlyArray; + + teamsService.getTeams().subscribe(result => { + teams = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush([ + teamResponse(12), + teamResponse(13), + ]); + + expect(teams!).toEqual([createTeam(12), createTeam(13)]); + })); + + it('should make get request to get team', + inject([TeamsService, HttpTestingController], (teamsService: TeamsService, httpMock: HttpTestingController) => { + let team: TeamDto; + + teamsService.getTeam('my-team').subscribe(result => { + team = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush(teamResponse(12)); + + expect(team!).toEqual(createTeam(12)); + })); + + it('should make post request to create team', + inject([TeamsService, HttpTestingController], (teamsService: TeamsService, httpMock: HttpTestingController) => { + const dto = { name: 'new-team' }; + + let team: TeamDto; + + teamsService.postTeam(dto).subscribe(result => { + team = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams'); + + expect(req.request.method).toEqual('POST'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush(teamResponse(12)); + + expect(team!).toEqual(createTeam(12)); + })); + + it('should make put request to update team', + inject([TeamsService, HttpTestingController], (teamsService: TeamsService, httpMock: HttpTestingController) => { + const resource: Resource = { + _links: { + update: { method: 'PUT', href: '/api/teams/my-team' }, + }, + }; + + let team: TeamDto; + + teamsService.putTeam('my-team', resource, { }, version).subscribe(result => { + team = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toBe(version.value); + + req.flush(teamResponse(12)); + + expect(team!).toEqual(createTeam(12)); + })); + + it('should make delete request to leave team', + inject([TeamsService, HttpTestingController], (teamsService: TeamsService, httpMock: HttpTestingController) => { + const resource: Resource = { + _links: { + leave: { method: 'DELETE', href: '/api/teams/my-team/contributors/me' }, + }, + }; + + teamsService.leaveTeam('my-team', resource).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team/contributors/me'); + + expect(req.request.method).toEqual('DELETE'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush({}); + })); + + function teamResponse(id: number, suffix = '') { + const key = `${id}${suffix}`; + + return { + id: `id${id}`, + created: `${id % 1000 + 2000}-12-12T10:10:00Z`, + createdBy: `creator${id}`, + lastModified: `${id % 1000 + 2000}-11-11T10:10:00Z`, + lastModifiedBy: `modifier${id}`, + version: key, + name: `team-name${key}`, + roleName: `Role${id}`, + roleProperties: createProperties(id), + _links: { + update: { method: 'PUT', href: `teams/${id}` }, + }, + }; + } +}); + +export function createTeam(id: number, suffix = '') { + const links: ResourceLinks = { + update: { method: 'PUT', href: `teams/${id}` }, + }; + + const key = `${id}${suffix}`; + + return new TeamDto(links, + `id${id}`, + DateTime.parseISO(`${id % 1000 + 2000}-12-12T10:10:00Z`), `creator${id}`, + DateTime.parseISO(`${id % 1000 + 2000}-11-11T10:10:00Z`), `modifier${id}`, + new Version(key), + `team-name${key}`, + `Role${id}`, + createProperties(id)); +} + +function createProperties(id: number) { + const result = {}; + + result[`property${id}`] = true; + + return result; +} diff --git a/frontend/src/app/shared/services/teams.service.ts b/frontend/src/app/shared/services/teams.service.ts new file mode 100644 index 000000000..d644bd25e --- /dev/null +++ b/frontend/src/app/shared/services/teams.service.ts @@ -0,0 +1,123 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ApiUrlConfig, DateTime, hasAnyLink, HTTP, pretifyError, Resource, ResourceLinks, Version } from '@app/framework'; + +export class TeamDto { + public readonly _links: ResourceLinks; + + public readonly canReadContributors: boolean; + public readonly canReadPlans: boolean; + public readonly canUpdateGeneral: boolean; + + constructor(links: ResourceLinks, + public readonly id: string, + public readonly created: DateTime, + public readonly createdBy: string, + public readonly lastModified: DateTime, + public readonly lastModifiedBy: string, + public readonly version: Version, + public readonly name: string, + public readonly roleName: string, + public readonly roleProperties: {}, + ) { + this._links = links; + + this.canReadContributors = hasAnyLink(links, 'contributors'); + this.canReadPlans = hasAnyLink(links, 'plans'); + this.canUpdateGeneral = hasAnyLink(links, 'update'); + } +} + +export type CreateTeamDto = Readonly<{ + // The new name of the team. Must not be unique. + name: string; +}>; + +export type UpdateTeamDto = Readonly<{ + // The new name of the team. Must not be unique. + name?: string; +}>; + +@Injectable() +export class TeamsService { + constructor( + private readonly http: HttpClient, + private readonly apiUrl: ApiUrlConfig, + ) { + } + + public getTeams(): Observable> { + const url = this.apiUrl.buildUrl('/api/teams'); + + return this.http.get(url).pipe( + map(body => { + const teams = body.map(parseTeam); + + return teams; + }), + pretifyError('i18n:teams.loadFailed')); + } + + public getTeam(teamName: string): Observable { + const url = this.apiUrl.buildUrl(`/api/teams/${teamName}`); + + return this.http.get(url).pipe( + map(body => { + const team = parseTeam(body); + + return team; + }), + pretifyError('i18n:teams.teamLoadFailed')); + } + + public postTeam(dto: CreateTeamDto): Observable { + const url = this.apiUrl.buildUrl('api/teams'); + + return this.http.post(url, dto).pipe( + map(body => { + return parseTeam(body); + }), + pretifyError('i18n:teams.createFailed')); + } + + public putTeam(teamId: string, resource: Resource, dto: UpdateTeamDto, version: Version): Observable { + const link = resource._links['update']; + + const url = this.apiUrl.buildUrl(link.href); + + return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + map(({ payload }) => { + return parseTeam(payload.body); + }), + pretifyError('i18n:teams.updateFailed')); + } + + public leaveTeam(teamId: string, resource: Resource): Observable { + const link = resource._links['leave']; + + const url = this.apiUrl.buildUrl(link.href); + + return this.http.request(link.method, url).pipe( + pretifyError('i18n:teams.leaveFailed')); + } +} + +function parseTeam(response: any & Resource) { + return new TeamDto(response._links, + response.id, + DateTime.parseISO(response.created), response.createdBy, + DateTime.parseISO(response.lastModified), response.lastModifiedBy, + new Version(response.version.toString()), + response.name, + response.roleName, + response.roleProperties); +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/usages.service.spec.ts b/frontend/src/app/shared/services/usages.service.spec.ts index 37ba0aacd..4783d3dbb 100644 --- a/frontend/src/app/shared/services/usages.service.spec.ts +++ b/frontend/src/app/shared/services/usages.service.spec.ts @@ -39,41 +39,27 @@ describe('UsagesService', () => { expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); - req.flush({ - allowedBytes: 512, - allowedCalls: 100, - blockingCalls: 200, - totalBytes: 1024, - totalCalls: 40, - monthCalls: 5120, - monthBytes: 256, - averageElapsedMs: 12.4, - details: { - category1: [ - { - date: '2017-10-12', - totalBytes: 10, - totalCalls: 130, - averageElapsedMs: 12.3, - }, - { - date: '2017-10-13', - totalBytes: 13, - totalCalls: 170, - averageElapsedMs: 33.3, - }, - ], - }, + req.flush(callsUsageResponse()); + + expect(usages!).toEqual(callsUsageResult()); + })); + + it('should make get request to get calls usages for team', + inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => { + let usages: CallsUsageDto; + + usagesService.getCallsUsagesForTeam('my-team', '2017-10-12', '2017-10-13').subscribe(result => { + usages = result; }); - expect(usages!).toEqual( - new CallsUsageDto(512, 100, 200, 1024, 40, 256, 5120, 12.4, { - category1: [ - new CallsUsagePerDateDto(DateTime.parseISO('2017-10-12'), 10, 130, 12.3), - new CallsUsagePerDateDto(DateTime.parseISO('2017-10-13'), 13, 170, 33.3), - ], - }), - ); + const req = httpMock.expectOne('http://service/p/api/teams/my-team/usages/calls/2017-10-12/2017-10-13'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush(callsUsageResponse()); + + expect(usages!).toEqual(callsUsageResult()); })); it('should make get request to get storage usages', @@ -89,24 +75,27 @@ describe('UsagesService', () => { expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); - req.flush([ - { - date: '2017-10-12', - totalCount: 10, - totalSize: 130, - }, - { - date: '2017-10-13', - totalCount: 13, - totalSize: 170, - }, - ]); + req.flush(storageUsageResponse()); + + expect(usages!).toEqual(storageUsageResult()); + })); + + it('should make get request to get storage usages for team', + inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => { + let usages: ReadonlyArray; + + usagesService.getStorageUsagesForTeam('my-team', '2017-10-12', '2017-10-13').subscribe(result => { + usages = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team/usages/storage/2017-10-12/2017-10-13'); - expect(usages!).toEqual( - [ - new StorageUsagePerDateDto(DateTime.parseISO('2017-10-12'), 10, 130), - new StorageUsagePerDateDto(DateTime.parseISO('2017-10-13'), 13, 170), - ]); + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush(storageUsageResponse()); + + expect(usages!).toEqual(storageUsageResult()); })); it('should make get request to get today storage', @@ -127,6 +116,24 @@ describe('UsagesService', () => { expect(usages!).toEqual(new CurrentStorageDto(130, 150)); })); + it('should make get request to get today storage for team', + inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => { + let usages: CurrentStorageDto; + + usagesService.getTodayStorageForTeam('my-team').subscribe(result => { + usages = result; + }); + + const req = httpMock.expectOne('http://service/p/api/teams/my-team/usages/storage/today'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush({ size: 130, maxAllowed: 150 }); + + expect(usages!).toEqual(new CurrentStorageDto(130, 150)); + })); + it('should make get request to get log', inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => { let url: string; @@ -145,3 +152,63 @@ describe('UsagesService', () => { expect(url!).toEqual('download/url'); })); }); + +function callsUsageResponse() { + return { + allowedBytes: 512, + allowedCalls: 100, + blockingCalls: 200, + totalBytes: 1024, + totalCalls: 40, + monthCalls: 5120, + monthBytes: 256, + averageElapsedMs: 12.4, + details: { + category1: [ + { + date: '2017-10-12', + totalBytes: 10, + totalCalls: 130, + averageElapsedMs: 12.3, + }, + { + date: '2017-10-13', + totalBytes: 13, + totalCalls: 170, + averageElapsedMs: 33.3, + }, + ], + }, + }; +} + +function callsUsageResult() { + return new CallsUsageDto(512, 100, 200, 1024, 40, 256, 5120, 12.4, { + category1: [ + new CallsUsagePerDateDto(DateTime.parseISO('2017-10-12'), 10, 130, 12.3), + new CallsUsagePerDateDto(DateTime.parseISO('2017-10-13'), 13, 170, 33.3), + ], + }); +} + +function storageUsageResponse() { + return [ + { + date: '2017-10-12', + totalCount: 10, + totalSize: 130, + }, + { + date: '2017-10-13', + totalCount: 13, + totalSize: 170, + }, + ]; +} + +function storageUsageResult() { + return [ + new StorageUsagePerDateDto(DateTime.parseISO('2017-10-12'), 10, 130), + new StorageUsagePerDateDto(DateTime.parseISO('2017-10-13'), 13, 170), + ]; +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/usages.service.ts b/frontend/src/app/shared/services/usages.service.ts index 8b733df9f..33fc66c8d 100644 --- a/frontend/src/app/shared/services/usages.service.ts +++ b/frontend/src/app/shared/services/usages.service.ts @@ -61,8 +61,8 @@ export class UsagesService { ) { } - public getLog(app: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/log`); + public getLog(appName: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/usages/log`); return this.http.get(url).pipe( map(body => { @@ -71,8 +71,8 @@ export class UsagesService { pretifyError('i18n:usages.loadMonthlyCallsFailed')); } - public getTodayStorage(app: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/today`); + public getTodayStorage(appName: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/usages/storage/today`); return this.http.get(url).pipe( map(body => { @@ -81,8 +81,28 @@ export class UsagesService { pretifyError('i18n:usages.loadTodayStorageFailed')); } - public getCallsUsages(app: string, fromDate: string, toDate: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/${fromDate}/${toDate}`); + public getTodayStorageForTeam(teamId: string): Observable { + const url = this.apiUrl.buildUrl(`api/teams/${teamId}/usages/storage/today`); + + return this.http.get(url).pipe( + map(body => { + return parseCurrentStorage(body); + }), + pretifyError('i18n:usages.loadTodayStorageFailed')); + } + + public getCallsUsages(appName: string, fromDate: string, toDate: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/usages/calls/${fromDate}/${toDate}`); + + return this.http.get(url).pipe( + map(body => { + return parseCallsUsage(body); + }), + pretifyError('i18n:usages.loadCallsFailed')); + } + + public getCallsUsagesForTeam(teamId: string, fromDate: string, toDate: string): Observable { + const url = this.apiUrl.buildUrl(`api/teams/${teamId}/usages/calls/${fromDate}/${toDate}`); return this.http.get(url).pipe( map(body => { @@ -91,8 +111,18 @@ export class UsagesService { pretifyError('i18n:usages.loadCallsFailed')); } - public getStorageUsages(app: string, fromDate: string, toDate: string): Observable> { - const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/${fromDate}/${toDate}`); + public getStorageUsages(appName: string, fromDate: string, toDate: string): Observable> { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/usages/storage/${fromDate}/${toDate}`); + + return this.http.get(url).pipe( + map(body => { + return parseStorageUser(body); + }), + pretifyError('i18n:usages.loadStorageFailed')); + } + + public getStorageUsagesForTeam(teamId: string, fromDate: string, toDate: string): Observable> { + const url = this.apiUrl.buildUrl(`api/teams/${teamId}/usages/storage/${fromDate}/${toDate}`); return this.http.get(url).pipe( map(body => { diff --git a/frontend/src/app/shared/state/_test-helpers.ts b/frontend/src/app/shared/state/_test-helpers.ts index 444aa15a5..0335d9035 100644 --- a/frontend/src/app/shared/state/_test-helpers.ts +++ b/frontend/src/app/shared/state/_test-helpers.ts @@ -7,13 +7,14 @@ import { of } from 'rxjs'; import { Mock } from 'typemoq'; -import { AppsState, AuthService, DateTime, FieldPropertiesDto, FieldRule, NestedFieldDto, RootFieldDto, SchemaDto, SchemaPropertiesDto, Version } from './../'; +import { AppsState, AuthService, DateTime, FieldPropertiesDto, FieldRule, NestedFieldDto, RootFieldDto, SchemaDto, SchemaPropertiesDto, TeamsState, Version } from './../'; const app = 'my-app'; const creation = DateTime.today().addDays(-2); const creator = 'me'; const modified = DateTime.now().addDays(-1); const modifier = 'now-me'; +const team = 'my-team'; const version = new Version('1'); const newVersion = new Version('2'); @@ -25,6 +26,14 @@ appsState.setup(x => x.appName) appsState.setup(x => x.selectedApp) .returns(() => of({ name: app })); +const teamsState = Mock.ofType(); + +teamsState.setup(x => x.teamId) + .returns(() => team); + +teamsState.setup(x => x.selectedTeam) + .returns(() => of({ id: team })); + const authService = Mock.ofType(); authService.setup(x => x.user) @@ -93,5 +102,7 @@ export const TestValues = { modified, modifier, newVersion, + team, + teamsState, version, }; diff --git a/frontend/src/app/shared/state/apps.forms.ts b/frontend/src/app/shared/state/apps.forms.ts index a6fc6a67a..a690ae423 100644 --- a/frontend/src/app/shared/state/apps.forms.ts +++ b/frontend/src/app/shared/state/apps.forms.ts @@ -9,7 +9,7 @@ import { FormControl, Validators } from '@angular/forms'; import { ExtendedFormGroup, Form, TemplatedFormArray, ValidatorsEx } from '@app/framework'; -import { AppDto, AppSettingsDto, CreateAppDto, UpdateAppDto, UpdateAppSettingsDto } from './../services/apps.service'; +import { AppDto, AppSettingsDto, CreateAppDto, TransferToTeamDto, UpdateAppDto, UpdateAppSettingsDto } from './../services/apps.service'; export class CreateAppForm extends Form { constructor() { @@ -23,6 +23,16 @@ export class CreateAppForm extends Form { } } +export class TransferAppForm extends Form { + constructor() { + super(new ExtendedFormGroup({ + teamId: new FormControl('', + Validators.required, + ), + })); + } +} + export class UpdateAppForm extends Form { constructor() { super(new ExtendedFormGroup({ diff --git a/frontend/src/app/shared/state/apps.state.ts b/frontend/src/app/shared/state/apps.state.ts index 8d07be453..e2bc7c714 100644 --- a/frontend/src/app/shared/state/apps.state.ts +++ b/frontend/src/app/shared/state/apps.state.ts @@ -137,6 +137,14 @@ export class AppsState extends State { shareSubscribed(this.dialogs, { silent: true })); } + public transfer(app: AppDto, teamId: string | null): Observable { + return this.appsService.transferApp(app.name, app, { teamId }, app.version).pipe( + tap(updated => { + this.replaceApp(updated); + }), + shareSubscribed(this.dialogs, { silent: true })); + } + public updateSettings(settings: AppSettingsDto, request: UpdateAppSettingsDto): Observable { return this.appsService.putSettings(this.appName, settings, request, settings.version).pipe( tap(updated => { diff --git a/frontend/src/app/shared/state/asset-scripts.state.ts b/frontend/src/app/shared/state/asset-scripts.state.ts index a39a69d1a..fa57fd87e 100644 --- a/frontend/src/app/shared/state/asset-scripts.state.ts +++ b/frontend/src/app/shared/state/asset-scripts.state.ts @@ -8,23 +8,17 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { DialogService, Resource, shareSubscribed, State, Version } from '@app/framework'; +import { DialogService, LoadingState, Resource, shareSubscribed, State, Version } from '@app/framework'; import { AppsService, AssetScripts, AssetScriptsPayload } from '../services/apps.service'; import { AppsState } from './apps.state'; -interface Snapshot { +interface Snapshot extends LoadingState { // The current scripts. scripts: AssetScripts; // The app version. version: Version; - // Indicates if the scripts are loaded. - isLoaded?: boolean; - - // Indicates if the scripts are loading. - isLoading?: boolean; - // Indicates if the user can update the scripts. canUpdate?: boolean; diff --git a/frontend/src/app/shared/state/asset-uploader.state.ts b/frontend/src/app/shared/state/asset-uploader.state.ts index 6c94348a2..60af65946 100644 --- a/frontend/src/app/shared/state/asset-uploader.state.ts +++ b/frontend/src/app/shared/state/asset-uploader.state.ts @@ -31,14 +31,11 @@ export interface Upload { interface Snapshot { // The uploads. - uploads: UploadList; + uploads: ReadonlyArray; } export class UploadCanceled {} -type UploadList = ReadonlyArray; -type UploadResult = AssetDto | number; - @Injectable() export class AssetUploaderState extends State { public uploads = @@ -70,7 +67,7 @@ export class AssetUploaderState extends State { }, 'Stopped'); } - public uploadFile(file: File, target?: AssetsState, parentId?: string): Observable { + public uploadFile(file: File, target?: AssetsState, parentId?: string): Observable { const stream = this.assetsService.postAssetFile(this.appName, file, parentId ?? target?.parentId); return this.upload(stream, MathHelper.guid(), file.name, asset => { @@ -84,7 +81,7 @@ export class AssetUploaderState extends State { }); } - public uploadAsset(asset: AssetDto, file: Blob): Observable { + public uploadAsset(asset: AssetDto, file: Blob): Observable { const stream = this.assetsService.putAssetFile(this.appName, asset, file, asset.version); return this.upload(stream, asset.id, file['name'] || asset.fileName); diff --git a/frontend/src/app/shared/state/backups.state.ts b/frontend/src/app/shared/state/backups.state.ts index 1753ece1e..de91eeb22 100644 --- a/frontend/src/app/shared/state/backups.state.ts +++ b/frontend/src/app/shared/state/backups.state.ts @@ -8,26 +8,18 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { DialogService, shareSubscribed, State } from '@app/framework'; +import { DialogService, LoadingState, shareSubscribed, State } from '@app/framework'; import { BackupDto, BackupsService } from './../services/backups.service'; import { AppsState } from './apps.state'; -interface Snapshot { +interface Snapshot extends LoadingState { // The current backups. - backups: BackupsList; - - // Indicates if the backups are loaded. - isLoaded?: boolean; - - // Indicates if the backups are loading. - isLoading?: boolean; + backups: ReadonlyArray; // Indicates if the user can create new backups. canCreate?: boolean; } -type BackupsList = ReadonlyArray; - @Injectable() export class BackupsState extends State { public backups = diff --git a/frontend/src/app/shared/state/clients.state.ts b/frontend/src/app/shared/state/clients.state.ts index 22976bb58..9517c0651 100644 --- a/frontend/src/app/shared/state/clients.state.ts +++ b/frontend/src/app/shared/state/clients.state.ts @@ -8,29 +8,21 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { DialogService, shareSubscribed, State, Version } from '@app/framework'; +import { DialogService, LoadingState, shareSubscribed, State, Version } from '@app/framework'; import { ClientDto, ClientsPayload, ClientsService, CreateClientDto, UpdateClientDto } from './../services/clients.service'; import { AppsState } from './apps.state'; -interface Snapshot { +interface Snapshot extends LoadingState { // The current clients. - clients: ClientsList; + clients: ReadonlyArray; // The app version. version: Version; - // Indicates if the clients are loaded. - isLoaded?: boolean; - - // Indicates if the clients are loading. - isLoading?: boolean; - // Indicates if the user can create new clients. canCreate?: boolean; } -type ClientsList = ReadonlyArray; - @Injectable() export class ClientsState extends State { public clients = diff --git a/frontend/src/app/shared/state/comments.state.ts b/frontend/src/app/shared/state/comments.state.ts index a57b8ce51..5955c8e9c 100644 --- a/frontend/src/app/shared/state/comments.state.ts +++ b/frontend/src/app/shared/state/comments.state.ts @@ -7,26 +7,24 @@ import { Observable } from 'rxjs'; import { map, tap } from 'rxjs/operators'; -import { DateTime, DialogService, shareSubscribed, State, Version } from '@app/framework'; +import { DateTime, DialogService, LoadingState, shareSubscribed, State, Version } from '@app/framework'; import { CommentDto, CommentsService } from './../services/comments.service'; -interface Snapshot { +interface Snapshot extends LoadingState { // The current comments. - comments: CommentsList; + comments: ReadonlyArray; // The version of the comments state. version: Version; - - // Indicates if the comments are loaded. - isLoaded?: boolean; } -type CommentsList = ReadonlyArray; - export class CommentsState extends State { public comments = this.project(x => x.comments); + public isLoading = + this.project(x => x.isLoading === true); + public isLoaded = this.project(x => x.isLoaded === true); diff --git a/frontend/src/app/shared/state/languages.state.ts b/frontend/src/app/shared/state/languages.state.ts index 6eae3bd98..15830e724 100644 --- a/frontend/src/app/shared/state/languages.state.ts +++ b/frontend/src/app/shared/state/languages.state.ts @@ -8,7 +8,7 @@ import { Injectable } from '@angular/core'; import { forkJoin, Observable } from 'rxjs'; import { finalize, map, shareReplay, tap } from 'rxjs/operators'; -import { DialogService, shareMapSubscribed, shareSubscribed, State, Version } from '@app/framework'; +import { DialogService, LoadingState, shareMapSubscribed, shareSubscribed, State, Version } from '@app/framework'; import { AppLanguageDto, AppLanguagesPayload, AppLanguagesService, UpdateAppLanguageDto } from './../services/app-languages.service'; import { LanguageDto, LanguagesService } from './../services/languages.service'; import { AppsState } from './apps.state'; @@ -18,39 +18,29 @@ export interface SnapshotLanguage { language: AppLanguageDto; // All configured fallback languages. - fallbackLanguages: LanguageList; + fallbackLanguages: ReadonlyArray; // The fallback languages that have not been added yet. - fallbackLanguagesNew: LanguageList; + fallbackLanguagesNew: ReadonlyArray; } -interface Snapshot { +interface Snapshot extends LoadingState { // All supported languages. - allLanguages: LanguageList; + allLanguages: ReadonlyArray; // The languages that have not been added yet. - allLanguagesNew: LanguageList; + allLanguagesNew: ReadonlyArray; // The configured languages with extra information. - languages: LanguageResultList; + languages: ReadonlyArray; // The app version. version: Version; - // Indicates if the languages are loaded. - isLoaded?: boolean; - - // Indicates if the languages are loading. - isLoading?: boolean; - // Inedicates if the user can add a language. canCreate?: boolean; } -type AppLanguagesList = ReadonlyArray; -type LanguageList = ReadonlyArray; -type LanguageResultList = ReadonlyArray; - @Injectable() export class LanguagesState extends State { private cachedLanguage$?: Observable>; @@ -154,7 +144,7 @@ export class LanguagesState extends State { shareMapSubscribed(this.dialogs, x => x.payload)); } - private replaceLanguages(payload: AppLanguagesPayload, version: Version, allLanguages?: LanguageList) { + private replaceLanguages(payload: AppLanguagesPayload, version: Version, allLanguages?: ReadonlyArray) { this.next(s => { allLanguages = allLanguages || s.allLanguages; @@ -191,7 +181,7 @@ export class LanguagesState extends State { return this.cachedLanguage$; } - private createLanguage(language: AppLanguageDto, languages: AppLanguagesList): SnapshotLanguage { + private createLanguage(language: AppLanguageDto, languages: ReadonlyArray): SnapshotLanguage { return { language, fallbackLanguages: diff --git a/frontend/src/app/shared/state/plans.state.ts b/frontend/src/app/shared/state/plans.state.ts index a32824d7a..972897084 100644 --- a/frontend/src/app/shared/state/plans.state.ts +++ b/frontend/src/app/shared/state/plans.state.ts @@ -8,7 +8,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { DialogService, shareSubscribed, State, Version } from '@app/framework'; +import { DialogService, LoadingState, shareSubscribed, State, Version } from '@app/framework'; import { AuthService } from './../services/auth.service'; import { PlanDto, PlansService } from './../services/plans.service'; import { AppsState } from './apps.state'; @@ -24,7 +24,7 @@ export interface PlanInfo { isSelected?: boolean; } -interface Snapshot { +interface Snapshot extends LoadingState { // The current plans. plans: ReadonlyArray; @@ -34,11 +34,8 @@ interface Snapshot { // The user, who owns the plan. planOwner?: string; - // Indicates if the plans are loaded. - isLoaded?: boolean; - - // Indicates if the plans are loading. - isLoading?: boolean; + // The ID of the team. + teamId?: string | null; // Indicates if there is a billing portal for the current Squidex instance. hasPortal?: boolean; @@ -58,6 +55,9 @@ export class PlansState extends State { public isOwner = this.project(x => x.isOwner === true); + public teamId = + this.project(x => x.teamId); + public isLoaded = this.project(x => x.isLoaded === true); @@ -65,7 +65,7 @@ export class PlansState extends State { this.project(x => x.isLoading === true); public isDisabled = - this.project(x => !x.isOwner); + this.project(x => !x.isOwner || x.teamId); public hasPortal = this.project(x => x.hasPortal); @@ -116,6 +116,7 @@ export class PlansState extends State { isOwner: !payload.planOwner || payload.planOwner === this.userId, planOwner: payload.planOwner, plans, + teamId: payload.teamId, version, }, 'Loading Success'); }), diff --git a/frontend/src/app/shared/state/roles.state.ts b/frontend/src/app/shared/state/roles.state.ts index 4bda077c4..59acb0ba4 100644 --- a/frontend/src/app/shared/state/roles.state.ts +++ b/frontend/src/app/shared/state/roles.state.ts @@ -8,29 +8,21 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { DialogService, shareSubscribed, State, Version } from '@app/framework'; +import { DialogService, LoadingState, shareSubscribed, State, Version } from '@app/framework'; import { CreateRoleDto, RoleDto, RolesPayload, RolesService, UpdateRoleDto } from './../services/roles.service'; import { AppsState } from './apps.state'; -interface Snapshot { +interface Snapshot extends LoadingState { // The current roles. - roles: RolesList; + roles: ReadonlyArray; // The app version. version: Version; - // Indicates if the roles are loaded. - isLoaded?: boolean; - - // Indicates if the roles are loading. - isLoading?: boolean; - // Indicates if the user can add a role. canCreate?: boolean; } -type RolesList = ReadonlyArray; - @Injectable() export class RolesState extends State { public roles = diff --git a/frontend/src/app/shared/state/rules.state.ts b/frontend/src/app/shared/state/rules.state.ts index 1c9cdc77c..a29280627 100644 --- a/frontend/src/app/shared/state/rules.state.ts +++ b/frontend/src/app/shared/state/rules.state.ts @@ -8,23 +8,17 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { finalize, map, tap } from 'rxjs/operators'; -import { DialogService, shareSubscribed, State } from '@app/framework'; +import { DialogService, LoadingState, shareSubscribed, State } from '@app/framework'; import { RuleDto, RulesService, UpsertRuleDto } from './../services/rules.service'; import { AppsState } from './apps.state'; -interface Snapshot { +interface Snapshot extends LoadingState { // The current rules. - rules: RulesList; + rules: ReadonlyArray; // The selected rule. selectedRule?: RuleDto | null; - // Indicates if the rules are loaded. - isLoaded?: boolean; - - // Indicates if the rules are loading. - isLoading?: boolean; - // The id of the rule that is currently running. runningRuleId?: string; @@ -38,8 +32,6 @@ interface Snapshot { canReadEvents?: boolean; } -type RulesList = ReadonlyArray; - @Injectable() export class RulesState extends State { public selectedRule = diff --git a/frontend/src/app/shared/state/schemas.state.ts b/frontend/src/app/shared/state/schemas.state.ts index 9d04cec24..36b219e3b 100644 --- a/frontend/src/app/shared/state/schemas.state.ts +++ b/frontend/src/app/shared/state/schemas.state.ts @@ -8,24 +8,18 @@ import { Injectable } from '@angular/core'; import { EMPTY, Observable, of } from 'rxjs'; import { catchError, finalize, tap } from 'rxjs/operators'; -import { DialogService, shareMapSubscribed, shareSubscribed, State, Version } from '@app/framework'; +import { DialogService, LoadingState, shareMapSubscribed, shareSubscribed, State, Version } from '@app/framework'; import { AddFieldDto, CreateSchemaDto, FieldDto, FieldRule, NestedFieldDto, RootFieldDto, SchemaDto, SchemasService, UpdateFieldDto, UpdateSchemaDto, UpdateUIFields } from './../services/schemas.service'; import { AppsState } from './apps.state'; type AnyFieldDto = NestedFieldDto | RootFieldDto; -interface Snapshot { +interface Snapshot extends LoadingState { // The schema categories. addedCategories: Set; // The current schemas. - schemas: SchemasList; - - // Indicates if the schemas are loaded. - isLoaded?: boolean; - - // Indicates if the schemas are loading. - isLoading?: boolean; + schemas: ReadonlyArray; // The selected schema. selectedSchema?: SchemaDto | null; @@ -34,8 +28,6 @@ interface Snapshot { canCreate?: boolean; } -export type SchemasList = ReadonlyArray; - @Injectable() export class SchemasState extends State { public selectedSchema = @@ -370,7 +362,7 @@ function getField(x: SchemaDto, request: AddFieldDto, parent?: RootFieldDto | nu } } -function buildCategoryNames(categories: Set, allSchemas: SchemasList): ReadonlyArray { +function buildCategoryNames(categories: Set, allSchemas: ReadonlyArray): ReadonlyArray { const uniqueCategories: { [name: string]: boolean } = {}; function addCategory(name: string) { diff --git a/frontend/src/app/shared/state/teams.forms.ts b/frontend/src/app/shared/state/teams.forms.ts new file mode 100644 index 000000000..01266456f --- /dev/null +++ b/frontend/src/app/shared/state/teams.forms.ts @@ -0,0 +1,34 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +/* eslint-disable no-useless-escape */ + +import { FormControl, Validators } from '@angular/forms'; +import { ExtendedFormGroup, Form } from '@app/framework'; +import { CreateTeamDto, TeamDto, UpdateTeamDto } from './../services/teams.service'; + +export class CreateTeamForm extends Form { + constructor() { + super(new ExtendedFormGroup({ + name: new FormControl('', [ + Validators.required, + Validators.maxLength(40), + ]), + })); + } +} + +export class UpdateTeamForm extends Form { + constructor() { + super(new ExtendedFormGroup({ + name: new FormControl('', [ + Validators.required, + Validators.maxLength(40), + ]), + })); + } +} diff --git a/frontend/src/app/shared/state/teams.state.spec.ts b/frontend/src/app/shared/state/teams.state.spec.ts new file mode 100644 index 000000000..ee43bfc59 --- /dev/null +++ b/frontend/src/app/shared/state/teams.state.spec.ts @@ -0,0 +1,144 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { firstValueFrom, of, throwError } from 'rxjs'; +import { IMock, Mock, Times } from 'typemoq'; +import { DialogService, TeamsService, TeamsState } from '@app/shared/internal'; +import { createTeam } from './../services/teams.service.spec'; + +describe('TeamsState', () => { + const team1 = createTeam(1); + const team2 = createTeam(2); + + let dialogs: IMock; + let teamsService: IMock; + let teamsState: TeamsState; + + beforeEach(() => { + dialogs = Mock.ofType(); + + teamsService = Mock.ofType(); + + teamsService.setup(x => x.getTeams()) + .returns(() => of([team1, team2])).verifiable(Times.atLeastOnce()); + + teamsState = new TeamsState(teamsService.object, dialogs.object); + teamsState.load().subscribe(); + }); + + afterEach(() => { + teamsService.verifyAll(); + }); + + it('should load teams', () => { + expect(teamsState.snapshot.teams).toEqual([team1, team2]); + }); + + it('should select team', async () => { + const teamSelect = await firstValueFrom(teamsState.select(team1.name)); + + expect(teamSelect).toBe(team1); + expect(teamsState.snapshot.selectedTeam).toBe(team1); + }); + + it('should return null on select if unselecting team', async () => { + const teamSelected = await firstValueFrom(teamsState.select(null)); + + expect(teamSelected).toBeNull(); + expect(teamsState.snapshot.selectedTeam).toBeNull(); + }); + + it('should return null on select if team is not found', async () => { + teamsService.setup(x => x.getTeam('unknown')) + .returns(() => throwError(() => 'Service Error')); + + const teamSelected = await firstValueFrom(teamsState.select('unknown')); + + expect(teamSelected).toBeNull(); + expect(teamsState.snapshot.selectedTeam).toBeNull(); + }); + + it('should return new team if loaded', async () => { + const newTeam = createTeam(1, '_new'); + + teamsService.setup(x => x.getTeam(team1.name)) + .returns(() => of(newTeam)); + + const teamSelected = await firstValueFrom(teamsState.loadTeam(team1.name)); + + expect(teamSelected).toEqual(newTeam); + expect(teamsState.snapshot.selectedTeam).toBeNull(); + }); + + it('should add team to snapshot if created', () => { + const updated = createTeam(3, '_new'); + + const request = { ...updated }; + + teamsService.setup(x => x.postTeam(request)) + .returns(() => of(updated)).verifiable(); + + teamsState.create(request).subscribe(); + + expect(teamsState.snapshot.teams).toEqual([team1, team2, updated]); + }); + + it('should update team if updated', () => { + const request = {}; + + const updated = createTeam(2, '_new'); + + teamsService.setup(x => x.putTeam(team2.name, team2, request, team2.version)) + .returns(() => of(updated)).verifiable(); + + teamsState.update(team2, request).subscribe(); + + expect(teamsState.snapshot.teams).toEqual([team1, updated]); + }); + + it('should remove team from snapshot if left', () => { + teamsService.setup(x => x.leaveTeam(team2.name, team2)) + .returns(() => of({})).verifiable(); + + teamsState.leave(team2).subscribe(); + + expect(teamsState.snapshot.teams).toEqual([team1]); + }); + + describe('Selection', () => { + beforeEach(() => { + teamsState.select(team1.name).subscribe(); + }); + + it('should update selected team if reloaded', () => { + const newTeams = [ + createTeam(1, '_new'), + createTeam(2, '_new'), + ]; + + teamsService.setup(x => x.getTeams()) + .returns(() => of(newTeams)); + + teamsState.load().subscribe(); + + expect(teamsState.snapshot.selectedTeam).toEqual(newTeams[0]); + }); + + it('should update selected team if updated', () => { + const request = {}; + + const updated = createTeam(1, '_new'); + + teamsService.setup(x => x.putTeam(team1.name, team1, request, team1.version)) + .returns(() => of(updated)).verifiable(); + + teamsState.update(team1, request).subscribe(); + + expect(teamsState.snapshot.selectedTeam).toEqual(updated); + }); + }); +}); diff --git a/frontend/src/app/shared/state/teams.state.ts b/frontend/src/app/shared/state/teams.state.ts new file mode 100644 index 000000000..eac2e5b20 --- /dev/null +++ b/frontend/src/app/shared/state/teams.state.ts @@ -0,0 +1,145 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; +import { DialogService, shareSubscribed, State } from '@app/framework'; +import { CreateTeamDto, TeamDto, TeamsService, UpdateTeamDto } from '@app/shared/internal'; + +interface Snapshot { + // All teams, loaded once. + teams: ReadonlyArray; + + // The selected team. + selectedTeam: TeamDto | null; +} + +@Injectable() +export class TeamsState extends State { + public teams = + this.project(s => s.teams); + + public selectedTeam = + this.project(s => s.selectedTeam); + + public get teamId() { + return this.snapshot.selectedTeam?.id || ''; + } + + constructor( + private readonly teamsService: TeamsService, + private readonly dialogs: DialogService, + ) { + super({ + teams: [], + selectedTeam: null, + }, 'Teams'); + } + + public reloadTeams() { + return this.loadTeam(this.teamId).pipe( + shareSubscribed(this.dialogs)); + } + + public select(name: string | null): Observable { + return this.loadTeam(name, true).pipe( + tap(selectedTeam => { + this.next({ selectedTeam }, 'Selected'); + })); + } + + public loadTeam(name: string | null, cached = false) { + if (!name) { + return of(null); + } + + if (cached) { + const found = this.snapshot.teams.find(x => x.name === name); + + if (found) { + return of(found); + } + } + + return this.teamsService.getTeam(name).pipe( + tap(team => { + this.replaceTeam(team); + }), + catchError(() => of(null))); + } + + public load(): Observable { + return this.teamsService.getTeams().pipe( + tap(teams => { + this.next(s => { + let selectedTeam = s.selectedTeam; + + if (selectedTeam) { + selectedTeam = teams.find(x => x.id === selectedTeam!.id) || selectedTeam; + } + + return { ...s, teams, selectedTeam }; + }, 'Loading Success'); + }), + shareSubscribed(this.dialogs)); + } + + public create(request: CreateTeamDto): Observable { + return this.teamsService.postTeam(request).pipe( + tap(created => { + this.next(s => { + const teams = [...s.teams, created].sortByString(x => x.name); + + return { ...s, teams }; + }, 'Created'); + }), + shareSubscribed(this.dialogs, { silent: true })); + } + + public update(team: TeamDto, request: UpdateTeamDto): Observable { + return this.teamsService.putTeam(team.name, team, request, team.version).pipe( + tap(updated => { + this.replaceTeam(updated); + }), + shareSubscribed(this.dialogs, { silent: true })); + } + + public leave(team: TeamDto): Observable { + return this.teamsService.leaveTeam(team.name, team).pipe( + tap(() => { + this.removeTeam(team); + }), + shareSubscribed(this.dialogs)); + } + + private removeTeam(team: TeamDto) { + this.next(s => { + const teams = s.teams.filter(x => x.name !== team.name); + + const selectedTeam = + s.selectedTeam?.id !== team.id ? + s.selectedTeam : + null; + + return { ...s, teams, selectedTeam }; + }, 'Deleted'); + } + + private replaceTeam(team: TeamDto) { + this.next(s => { + const teams = s.teams.replacedBy('id', team); + + const selectedTeam = + s.selectedTeam?.id !== team.id ? + s.selectedTeam : + team; + + return { ...s, teams, selectedTeam }; + }, 'Updated'); + } +} diff --git a/frontend/src/app/shared/state/templates.state.ts b/frontend/src/app/shared/state/templates.state.ts index 7acfd919f..bc4a069f8 100644 --- a/frontend/src/app/shared/state/templates.state.ts +++ b/frontend/src/app/shared/state/templates.state.ts @@ -8,25 +8,17 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { DialogService, shareSubscribed, State } from '@app/framework'; +import { DialogService, LoadingState, shareSubscribed, State } from '@app/framework'; import { TemplateDto, TemplatesService } from './../services/templates.service'; -interface Snapshot { +interface Snapshot extends LoadingState { // The current templates. - templates: TemplatesList; - - // Indicates if the templates are loaded. - isLoaded?: boolean; - - // Indicates if the templates are loading. - isLoading?: boolean; + templates: ReadonlyArray; // Indicates if the user can create new templates. canCreate?: boolean; } -type TemplatesList = ReadonlyArray; - @Injectable() export class TemplatesState extends State { public templates = diff --git a/frontend/src/app/shared/state/ui.state.ts b/frontend/src/app/shared/state/ui.state.ts index e89600931..d405f52dd 100644 --- a/frontend/src/app/shared/state/ui.state.ts +++ b/frontend/src/app/shared/state/ui.state.ts @@ -12,7 +12,7 @@ import { UIService } from './../services/ui.service'; import { UsersService } from './../services/users.service'; import { AppsState } from './apps.state'; -type Settings = { canCreateApps?: boolean; [key: string]: any }; +type Settings = { canCreateApps?: boolean; canCreateTeams?: boolean; [key: string]: any }; interface Snapshot { // All common settings. diff --git a/frontend/src/app/shared/state/workflows.state.ts b/frontend/src/app/shared/state/workflows.state.ts index 739e04204..288f10c08 100644 --- a/frontend/src/app/shared/state/workflows.state.ts +++ b/frontend/src/app/shared/state/workflows.state.ts @@ -8,11 +8,11 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { DialogService, shareSubscribed, State, Version } from '@app/framework'; +import { DialogService, LoadingState, shareSubscribed, State, Version } from '@app/framework'; import { WorkflowDto, WorkflowsPayload, WorkflowsService } from './../services/workflows.service'; import { AppsState } from './apps.state'; -interface Snapshot { +interface Snapshot extends LoadingState { // The current workflow. workflows: ReadonlyArray; @@ -22,12 +22,6 @@ interface Snapshot { // The errors. errors: ReadonlyArray; - // Indicates if the workflows are loaded. - isLoaded?: boolean; - - // Indicates if the workflows are loading. - isLoading?: boolean; - // Indicates if the user can create new workflow. canCreate?: boolean; } diff --git a/frontend/src/app/shell/declarations.ts b/frontend/src/app/shell/declarations.ts index 1d340062f..9bb1d5f95 100644 --- a/frontend/src/app/shell/declarations.ts +++ b/frontend/src/app/shell/declarations.ts @@ -16,6 +16,7 @@ export * from './pages/internal/notification-dropdown.component'; export * from './pages/internal/notifications-menu.component'; export * from './pages/internal/profile-menu.component'; export * from './pages/internal/search-menu.component'; +export * from './pages/internal/teams-area.component'; export * from './pages/login/login-page.component'; export * from './pages/logout/logout-page.component'; export * from './pages/not-found/not-found-page.component'; diff --git a/frontend/src/app/shell/module.ts b/frontend/src/app/shell/module.ts index ff3bb7639..ce6f6146b 100644 --- a/frontend/src/app/shell/module.ts +++ b/frontend/src/app/shell/module.ts @@ -7,7 +7,7 @@ import { NgModule } from '@angular/core'; import { SqxFrameworkModule, SqxSharedModule } from '@app/shared'; -import { AppAreaComponent, AppsMenuComponent, ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LeftMenuComponent, LoginPageComponent, LogoComponent, LogoutPageComponent, NotFoundPageComponent, NotificationDropdownComponent, NotificationsMenuComponent, ProfileMenuComponent, SearchMenuComponent } from './declarations'; +import { AppAreaComponent, AppsMenuComponent, ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LeftMenuComponent, LoginPageComponent, LogoComponent, LogoutPageComponent, NotFoundPageComponent, NotificationDropdownComponent, NotificationsMenuComponent, ProfileMenuComponent, SearchMenuComponent, TeamsAreaComponent } from './declarations'; @NgModule({ imports: [ @@ -36,6 +36,7 @@ import { AppAreaComponent, AppsMenuComponent, ForbiddenPageComponent, HomePageCo NotificationsMenuComponent, ProfileMenuComponent, SearchMenuComponent, + TeamsAreaComponent, ], }) export class SqxShellModule { } diff --git a/frontend/src/app/shell/pages/app/left-menu.component.ts b/frontend/src/app/shell/pages/app/left-menu.component.ts index b518ac8e3..c71cb7607 100644 --- a/frontend/src/app/shell/pages/app/left-menu.component.ts +++ b/frontend/src/app/shell/pages/app/left-menu.component.ts @@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { AppDto, Settings } from '@app/shared'; @Component({ - selector: 'sqx-left-menu', + selector: 'sqx-left-menu[app]', styleUrls: ['./left-menu.component.scss'], templateUrl: './left-menu.component.html', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/frontend/src/app/shell/pages/internal/apps-menu.component.html b/frontend/src/app/shell/pages/internal/apps-menu.component.html index 2133958a8..8538bacf9 100644 --- a/frontend/src/app/shell/pages/internal/apps-menu.component.html +++ b/frontend/src/app/shell/pages/internal/apps-menu.component.html @@ -4,47 +4,80 @@ {{app.displayName}} - + - {{ 'apps.appsButtonFallbackTitle' | sqxTranslate }} + + {{team.name}} + + + + {{ 'apps.appsButtonFallbackTitle' | sqxTranslate }} + - - - - -
-
-

{{ 'apps.allApps' | sqxTranslate }}

-
-
- {{apps.length}} -
-
-
- + + + - - + +
+
+ {{ 'apps.allApps' | sqxTranslate }} +
+
+ {{apps.length}} +
+
+
+
+
- + + -