From 45b0f61a7ba91bdff8ef74a36fc94621765182c1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 13 Jun 2019 23:03:45 +0100 Subject: [PATCH] Tests improved. --- .../Controllers/Apps/AppClientsController.cs | 42 ++++++--- .../Apps/AppContributorsController.cs | 13 ++- .../Apps/AppLanguagesController.cs | 8 +- .../Api/Controllers/Apps/AppsController.cs | 10 +- .../Controllers/Apps/Models/AppCreatedDto.cs | 59 ------------ .../Api/Controllers/Apps/Models/AppDto.cs | 46 ++++++--- .../Api/Controllers/Apps/Models/ClientDto.cs | 16 ++-- .../Api/Controllers/Apps/Models/ClientsDto.cs | 38 ++++++++ .../Controllers/Users/Models/ResourcesDto.cs | 6 ++ .../Api/Controllers/Users/UsersController.cs | 2 +- .../pages/rules/rules-page.component.scss | 4 - .../pages/more/more-page.component.ts | 2 +- .../services/app-languages.service.spec.ts | 8 +- .../app/shared/services/apps.service.spec.ts | 93 +++++++++++-------- .../app/shared/services/apps.service.ts | 29 +++--- .../app/shared/services/users.service.spec.ts | 2 +- .../app/shared/services/users.service.ts | 2 +- .../app/shared/state/apps.state.spec.ts | 51 +++++----- src/Squidex/app/shared/state/apps.state.ts | 36 +++---- .../app/shared/state/languages.state.spec.ts | 45 ++++----- .../app/shared/state/languages.state.ts | 2 +- .../shell/pages/app/left-menu.component.html | 4 +- 22 files changed, 273 insertions(+), 245 deletions(-) delete mode 100644 src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs create mode 100644 src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index a17673252..af59c84ae 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; 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.Infrastructure.Commands; using Squidex.Shared; @@ -41,12 +42,12 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/{app}/clients/")] - [ProducesResponseType(typeof(ClientDto[]), 200)] + [ProducesResponseType(typeof(ClientsDto), 200)] [ApiPermission(Permissions.AppClientsRead)] [ApiCosts(0)] public IActionResult GetClients(string app) { - var response = App.Clients.Select(ClientDto.FromKvp).ToArray(); + var response = ClientsDto.FromApp(App, this); Response.Headers[HeaderNames.ETag] = App.Version.ToString(); @@ -60,6 +61,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// Client object that needs to be added to the app. /// /// 201 => Client generated. + /// 400 => Client request not valid. /// 404 => App not found. /// /// @@ -68,16 +70,15 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPost] [Route("apps/{app}/clients/")] - [ProducesResponseType(typeof(ClientDto), 201)] + [ProducesResponseType(typeof(ClientsDto), 200)] + [ProducesResponseType(typeof(ErrorDto), 400)] [ApiPermission(Permissions.AppClientsCreate)] [ApiCosts(1)] public async Task PostClient(string app, [FromBody] CreateClientDto request) { var command = request.ToCommand(); - await CommandBus.PublishAsync(command); - - var response = ClientDto.FromCommand(command); + var response = await InvokeCommandAsync(command); return CreatedAtAction(nameof(GetClients), new { app }, response); } @@ -89,7 +90,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// The id of the client that must be updated. /// Client object that needs to be updated. /// - /// 204 => Client updated. + /// 200 => Client updated. /// 400 => Client request not valid. /// 404 => Client or app not found. /// @@ -98,13 +99,17 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPut] [Route("apps/{app}/clients/{clientId}/")] + [ProducesResponseType(typeof(ClientsDto), 200)] + [ProducesResponseType(typeof(ErrorDto), 400)] [ApiPermission(Permissions.AppClientsUpdate)] [ApiCosts(1)] public async Task PutClient(string app, string clientId, [FromBody] UpdateClientDto request) { - await CommandBus.PublishAsync(request.ToCommand(clientId)); + var command = request.ToCommand(clientId); + + var response = await InvokeCommandAsync(command); - return NoContent(); + return Ok(response); } /// @@ -113,7 +118,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// The name of the app. /// The id of the client that must be deleted. /// - /// 204 => Client revoked. + /// 200 => Client revoked. /// 404 => Client or app not found. /// /// @@ -121,13 +126,26 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/clients/{clientId}/")] + [ProducesResponseType(typeof(ClientsDto), 200)] [ApiPermission(Permissions.AppClientsDelete)] [ApiCosts(1)] public async Task DeleteClient(string app, string clientId) { - await CommandBus.PublishAsync(new RevokeClient { Id = clientId }); + var command = new RevokeClient { Id = clientId }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + private async Task InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command); + + var result = context.Result(); + var response = ClientsDto.FromApp(result, this); - return NoContent(); + return response; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index 79769db43..d3922eae7 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -109,11 +109,20 @@ namespace Squidex.Areas.Api.Controllers.Apps public async Task DeleteContributor(string app, string id) { var command = new RemoveContributor { ContributorId = id }; - var context = await CommandBus.PublishAsync(command); - var response = ContributorsDto.FromApp(context.Result(), appPlansProvider, this, false); + var response = await InvokeCommandAsync(command); return Ok(response); } + + private async Task InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command); + + var result = context.Result(); + var response = ContributorsDto.FromApp(result, appPlansProvider, this, false); + + return response; + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index ba55d5485..ed24f37bd 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Apps { var command = request.ToCommand(); - var response = await InvokeCommandAsync(app, command); + var response = await InvokeCommandAsync(command); return CreatedAtAction(nameof(GetLanguages), new { app }, response); } @@ -98,7 +98,7 @@ namespace Squidex.Areas.Api.Controllers.Apps { var command = request.ToCommand(ParseLanguage(language)); - var response = await InvokeCommandAsync(app, command); + var response = await InvokeCommandAsync(command); return Ok(response); } @@ -121,12 +121,12 @@ namespace Squidex.Areas.Api.Controllers.Apps { var command = new RemoveLanguage { Language = ParseLanguage(language) }; - var response = await InvokeCommandAsync(app, command); + var response = await InvokeCommandAsync(command); return Ok(response); } - private async Task InvokeCommandAsync(string app, ICommand command) + private async Task InvokeCommandAsync(ICommand command) { var context = await CommandBus.PublishAsync(command); diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 36f336ced..0cf66505f 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure; @@ -84,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPost] [Route("apps/")] - [ProducesResponseType(typeof(AppCreatedDto), 201)] + [ProducesResponseType(typeof(AppDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(ErrorDto), 409)] [ApiPermission] @@ -93,8 +94,11 @@ namespace Squidex.Areas.Api.Controllers.Apps { var context = await CommandBus.PublishAsync(request.ToCommand()); - var result = context.Result>(); - var response = AppCreatedDto.FromResult(request.Name, result, appPlansProvider); + var userOrClientId = HttpContext.User.UserOrClientId(); + var userPermissions = HttpContext.Permissions(); + + var result = context.Result(); + var response = AppDto.FromApp(result, userOrClientId, userPermissions, appPlansProvider, this); return CreatedAtAction(nameof(GetApps), response); } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs deleted file mode 100644 index 38e6adae1..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class AppCreatedDto - { - /// - /// Id of the created entity. - /// - [Required] - public string Id { get; set; } - - /// - /// The permission level of the user. - /// - public string[] Permissions { get; set; } - - /// - /// The new version of the entity. - /// - public long Version { get; set; } - - /// - /// Gets the current plan name. - /// - public string PlanName { get; set; } - - /// - /// Gets the next plan name. - /// - public string PlanUpgrade { get; set; } - - public static AppCreatedDto FromResult(string name, EntityCreatedResult result, IAppPlansProvider apps) - { - var response = new AppCreatedDto - { - Id = result.IdOrValue.ToString(), - Permissions = Role.CreateOwner(name).Permissions.ToIds().ToArray(), - PlanName = apps.GetPlan(null)?.Name, - PlanUpgrade = apps.GetPlanUpgrade(null)?.Name, - Version = result.Version - }; - - return response; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index a41ad2dd0..13b726623 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -8,9 +8,11 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using NodaTime; using Squidex.Areas.Api.Controllers.Assets; using Squidex.Areas.Api.Controllers.Backups; +using Squidex.Areas.Api.Controllers.Ping; using Squidex.Areas.Api.Controllers.Plans; using Squidex.Areas.Api.Controllers.Rules; using Squidex.Areas.Api.Controllers.Schemas; @@ -59,6 +61,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// public string[] Permissions { get; set; } + /// + /// Indicates if the user can access the api. + /// + public bool CanAccessApi { get; set; } + + /// + /// Indicates if the user can access at least one content. + /// + public bool CanAccessContent { get; set; } + /// /// Gets the current plan name. /// @@ -70,6 +82,26 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models public string PlanUpgrade { get; set; } public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans, ApiController controller) + { + var permissions = GetPermissions(app, userId, userPermissions); + + var result = SimpleMapper.Map(app, new AppDto()); + + result.Permissions = permissions.ToIds().ToArray(); + result.PlanName = plans.GetPlanForApp(app)?.Name; + + result.CanAccessApi = controller.HasPermission(AllPermissions.AppApi, app.Name, "*", permissions); + result.CanAccessContent = controller.HasPermission(AllPermissions.AppContentsRead, app.Name, "*", permissions); + + if (controller.HasPermission(AllPermissions.AppPlansChange, app.Name)) + { + result.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name; + } + + return CreateLinks(result, controller, permissions); + } + + private static PermissionSet GetPermissions(IAppEntity app, string userId, PermissionSet userPermissions) { var permissions = new List(); @@ -83,23 +115,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models permissions.AddRange(userPermissions.ToAppPermissions(app.Name)); } - var result = SimpleMapper.Map(app, new AppDto()); - - result.Permissions = permissions.ToArray(x => x.Id); - result.PlanName = plans.GetPlanForApp(app)?.Name; - - if (controller.HasPermission(AllPermissions.AppPlansChange, app.Name)) - { - result.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name; - } - - return CreateLinks(result, controller, new PermissionSet(permissions)); + return new PermissionSet(permissions); } private static AppDto CreateLinks(AppDto result, ApiController controller, PermissionSet permissions) { var values = new { app = result.Name }; + result.AddGetLink("ping", controller.Url(x => nameof(x.GetAppPing), values)); + if (controller.HasPermission(AllPermissions.AppDelete, result.Name, permissions: permissions)) { result.AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteApp), values)); diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs index 75d6b8da3..70c4e3649 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs @@ -5,16 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Reflection; -using Roles = Squidex.Domain.Apps.Core.Apps.Role; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps.Models { - public sealed class ClientDto + public sealed class ClientDto : Resource { /// /// The client id. @@ -39,14 +37,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// public string Role { get; set; } - public static ClientDto FromKvp(KeyValuePair kvp) + public static ClientDto FromClient(string id, AppClient client, ApiController controller, string app) { - return SimpleMapper.Map(kvp.Value, new ClientDto { Id = kvp.Key }); + var result = SimpleMapper.Map(client, new ClientDto { Id = id }); + + return CreateLinks(result, controller, app); } - public static ClientDto FromCommand(AttachClient command) + private static ClientDto CreateLinks(ClientDto result, ApiController controller, string app) { - return SimpleMapper.Map(command, new ClientDto { Name = command.Id, Role = Roles.Editor }); + return result; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs new file mode 100644 index 000000000..cf92b47ac --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class ClientsDto : Resource + { + /// + /// The clients. + /// + [Required] + public ClientDto[] Items { get; set; } + + public static ClientsDto FromApp(IAppEntity app, ApiController controller) + { + var result = new ClientsDto + { + Items = app.Clients.Select(x => ClientDto.FromClient(x.Key, x.Value, controller, app.Name)).ToArray() + }; + + return CreateLinks(result, controller, app.Name); + } + + private static ClientsDto CreateLinks(ClientsDto result, ApiController controller, string app) + { + return result; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs index 13570a1bf..885b4fba4 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs @@ -7,6 +7,8 @@ using Squidex.Areas.Api.Controllers.Backups; using Squidex.Areas.Api.Controllers.EventConsumers; +using Squidex.Areas.Api.Controllers.Languages; +using Squidex.Areas.Api.Controllers.Ping; using Squidex.Shared; using Squidex.Web; @@ -18,6 +20,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models { var result = new ResourcesDto(); + result.AddGetLink("ping", controller.Url(x => nameof(x.GetPing))); + + result.AddGetLink("languages", controller.Url(x => nameof(x.GetLanguages))); + if (controller.HasPermission(Permissions.AdminEventsRead)) { result.AddGetLink("admin/eventConsumers", controller.Url(x => nameof(x.GetEventConsumers))); diff --git a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs index 0ab2944c5..039ce367a 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -63,7 +63,7 @@ namespace Squidex.Areas.Api.Controllers.Users /// 200 => User resources returned. /// [HttpGet] - [Route("user/resources/")] + [Route("/")] [ProducesResponseType(typeof(ResourcesDto), 200)] [ApiPermission] public IActionResult GetUserResources() diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss b/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss index e4dc92750..7d868467c 100644 --- a/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss @@ -1,10 +1,6 @@ @import '_vars'; @import '_mixins'; -sqx-toggle { - display: inline-block; -} - .rule-element { display: block; } diff --git a/src/Squidex/app/features/settings/pages/more/more-page.component.ts b/src/Squidex/app/features/settings/pages/more/more-page.component.ts index c11a5011f..ce3befc0a 100644 --- a/src/Squidex/app/features/settings/pages/more/more-page.component.ts +++ b/src/Squidex/app/features/settings/pages/more/more-page.component.ts @@ -23,7 +23,7 @@ export class MorePageComponent { } public archiveApp() { - this.appsState.delete(this.appsState.appName) + this.appsState.delete(this.appsState.selectedAppState!) .subscribe(() => { this.router.navigate(['/app']); }); diff --git a/src/Squidex/app/shared/services/app-languages.service.spec.ts b/src/Squidex/app/shared/services/app-languages.service.spec.ts index a2909e80e..05d613ff8 100644 --- a/src/Squidex/app/shared/services/app-languages.service.spec.ts +++ b/src/Squidex/app/shared/services/app-languages.service.spec.ts @@ -101,7 +101,9 @@ describe('AppLanguagesService', () => { let languages: AppLanguagesDto; - appLanguagesService.putLanguage('my-app', resource, dto, version).subscribe(); + appLanguagesService.putLanguage('my-app', resource, dto, version).subscribe(result => { + languages = result; + }); const req = httpMock.expectOne('http://service/p/api/apps/my-app/languages/de'); @@ -122,7 +124,7 @@ describe('AppLanguagesService', () => { const resource: Resource = { _links: { - update: { method: 'PUT', href: 'api/apps/my-app/languages/de' } + update: { method: 'DELETE', href: 'api/apps/my-app/languages/de' } } }; @@ -149,7 +151,7 @@ describe('AppLanguagesService', () => { function languagesResponse(...codes: string[]) { return { items: codes.map((code, i) => ({ - code: code, + iso2Code: code, englishName: code, isMaster: i === 0, isOptional: i % 2 === 1, diff --git a/src/Squidex/app/shared/services/apps.service.spec.ts b/src/Squidex/app/shared/services/apps.service.spec.ts index 93dd5f089..ef1b4f3bb 100644 --- a/src/Squidex/app/shared/services/apps.service.spec.ts +++ b/src/Squidex/app/shared/services/apps.service.spec.ts @@ -11,10 +11,10 @@ import { inject, TestBed } from '@angular/core/testing'; import { AnalyticsService, ApiUrlConfig, - AppCreatedDto, AppDto, AppsService, - DateTime + DateTime, + Resource } from '@app/shared/internal'; describe('AppsService', () => { @@ -50,31 +50,11 @@ describe('AppsService', () => { expect(req.request.headers.get('If-Match')).toBeNull(); req.flush([ - { - id: '123', - name: 'name1', - permissions: ['Owner'], - created: '2016-01-01', - lastModified: '2016-02-02', - planName: 'Free', - planUpgrade: 'Basic' - }, - { - id: '456', - name: 'name2', - permissions: ['Owner'], - created: '2017-01-01', - lastModified: '2017-02-02', - planName: 'Basic', - planUpgrade: 'Enterprise' - } + appResponse(12), + appResponse(13) ]); - expect(apps!).toEqual( - [ - new AppDto('123', 'name1', ['Owner'], DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic'), - new AppDto('456', 'name2', ['Owner'], DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Basic', 'Enterprise') - ]); + expect(apps!).toEqual([createApp(12), createApp(13)]); })); it('should make post request to create app', @@ -82,7 +62,7 @@ describe('AppsService', () => { const dto = { name: 'new-app' }; - let app: AppCreatedDto; + let app: AppDto; appsService.postApp(dto).subscribe(result => { app = result; @@ -93,25 +73,21 @@ describe('AppsService', () => { expect(req.request.method).toEqual('POST'); expect(req.request.headers.get('If-Match')).toBeNull(); - req.flush({ - id: '123', - permissions: ['Reader'], - planName: 'Basic', - planUpgrade: 'Enterprise' - }); + req.flush(appResponse(12)); - expect(app!).toEqual({ - id: '123', - permissions: ['Reader'], - planName: 'Basic', - planUpgrade: 'Enterprise' - }); + expect(app!).toEqual(createApp(12)); })); it('should make delete request to archive app', - inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { + inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { - appsService.deleteApp('my-app').subscribe(); + const resource: Resource = { + _links: { + delete: { method: 'DELETE', href: '/api/apps/my-app' } + } + }; + + appsService.deleteApp(resource).subscribe(); const req = httpMock.expectOne('http://service/p/api/apps/my-app'); @@ -120,4 +96,39 @@ describe('AppsService', () => { req.flush({}); })); -}); \ No newline at end of file + + function appResponse(id: number, suffix = '') { + return { + id: `id${id}`, + name: `name${id}${suffix}`, + permissions: ['Owner'], + created: `${id % 1000 + 2000}-12-12T10:10`, + lastModified: `${id % 1000 + 2000}-11-11T10:10`, + canAccessApi: id % 2 === 0, + canAccessContent: id % 2 === 0, + planName: 'Free', + planUpgrade: 'Basic', + _links: { + schemas: { method: 'GET', href: '/schemas' } + } + }; + } +}); + +export function createApp(id: number, suffix = '') { + const result = new AppDto( + `id${id}`, + `name${id}${suffix}`, + ['Owner'], + DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10`), + DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10`), + id % 2 === 0, + id % 2 === 0, + 'Free', + 'Basic' + ); + + result._links['schemas'] = { method: 'GET', href: '/schemas' }; + + return result; +} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps.service.ts b/src/Squidex/app/shared/services/apps.service.ts index e55eabdb9..44c3dda9f 100644 --- a/src/Squidex/app/shared/services/apps.service.ts +++ b/src/Squidex/app/shared/services/apps.service.ts @@ -15,6 +15,7 @@ import { ApiUrlConfig, DateTime, pretifyError, + Resource, ResourceLinks, withLinks } from '@app/framework'; @@ -28,6 +29,8 @@ export class AppDto { public readonly permissions: string[], public readonly created: DateTime, public readonly lastModified: DateTime, + public readonly canAccessApi: boolean, + public readonly canAccessContent: boolean, public readonly planName?: string, public readonly planUpgrade?: string ) { @@ -39,13 +42,6 @@ export interface CreateAppDto { readonly template?: string; } -export interface AppCreatedDto { - readonly id: string; - readonly permissions: string[]; - readonly planName?: string; - readonly planUpgrade?: string; -} - @Injectable() export class AppsService { constructor( @@ -67,22 +63,27 @@ export class AppsService { pretifyError('Failed to load apps. Please reload.')); } - public postApp(dto: CreateAppDto): Observable { + public postApp(dto: CreateAppDto): Observable { const url = this.apiUrl.buildUrl('api/apps'); - return this.http.post(url, dto).pipe( + return this.http.post(url, dto).pipe( + map(body => { + return parseApp(body); + }), tap(() => { this.analytics.trackEvent('App', 'Created', dto.name); }), pretifyError('Failed to create app. Please reload.')); } - public deleteApp(appName: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}`); + public deleteApp(resource: Resource): Observable { + const link = resource._links['delete']; + + const url = this.apiUrl.buildUrl(link.href); - return this.http.delete(url).pipe( + return this.http.request(link.method, url).pipe( tap(() => { - this.analytics.trackEvent('App', 'Archived', appName); + this.analytics.trackEvent('App', 'Archived'); }), pretifyError('Failed to archive app. Please reload.')); } @@ -96,6 +97,8 @@ function parseApp(response: any) { response.permissions, DateTime.parseISO(response.created), DateTime.parseISO(response.lastModified), + response.canAccessApi, + response.canAccessContent, response.planName, response.planUpgrade), response); diff --git a/src/Squidex/app/shared/services/users.service.spec.ts b/src/Squidex/app/shared/services/users.service.spec.ts index 546199ccf..9a45881f0 100644 --- a/src/Squidex/app/shared/services/users.service.spec.ts +++ b/src/Squidex/app/shared/services/users.service.spec.ts @@ -124,7 +124,7 @@ describe('UsersService', () => { resources = result; }); - const req = httpMock.expectOne('http://service/p/api/user/resources'); + const req = httpMock.expectOne('http://service/p/api'); expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); diff --git a/src/Squidex/app/shared/services/users.service.ts b/src/Squidex/app/shared/services/users.service.ts index 77bfe8fa1..c3cfd6a4f 100644 --- a/src/Squidex/app/shared/services/users.service.ts +++ b/src/Squidex/app/shared/services/users.service.ts @@ -67,7 +67,7 @@ export class UsersService { } public getResources(): Observable { - const url = this.apiUrl.buildUrl(`api/user/resources`); + const url = this.apiUrl.buildUrl(`api`); return this.http.get(url).pipe( map(body => { diff --git a/src/Squidex/app/shared/state/apps.state.spec.ts b/src/Squidex/app/shared/state/apps.state.spec.ts index 0e19eacc7..68e1c71c8 100644 --- a/src/Squidex/app/shared/state/apps.state.spec.ts +++ b/src/Squidex/app/shared/state/apps.state.spec.ts @@ -12,19 +12,16 @@ import { AppDto, AppsService, AppsState, - DateTime, DialogService } from '@app/shared/internal'; -describe('AppsState', () => { - const now = DateTime.now(); +import { createApp } from '../services/apps.service.spec'; - const oldApps = [ - new AppDto('id1', 'old-name1', ['Owner'], now, now), - new AppDto('id2', 'old-name2', ['Owner'], now, now) - ]; +describe('AppsState', () => { + const app1 = createApp(1); + const app2 = createApp(2); - const newApp = new AppDto('id3', 'new-name', ['Owner'], now, now); + const newApp = createApp(3); let dialogs: IMock; let appsService: IMock; @@ -36,7 +33,7 @@ describe('AppsState', () => { appsService = Mock.ofType(); appsService.setup(x => x.getApps()) - .returns(() => of(oldApps)).verifiable(); + .returns(() => of([app1, app2])).verifiable(); appsState = new AppsState(appsService.object, dialogs.object); appsState.load().subscribe(); @@ -47,18 +44,18 @@ describe('AppsState', () => { }); it('should load apps', () => { - expect(appsState.snapshot.apps.values).toEqual(oldApps); + expect(appsState.snapshot.apps.values).toEqual([app1, app2]); }); it('should select app', () => { let selectedApp: AppDto; - appsState.select(oldApps[0].name).subscribe(x => { + appsState.select(app1.name).subscribe(x => { selectedApp = x!; }); - expect(selectedApp!).toBe(oldApps[0]); - expect(appsState.snapshot.selectedApp).toBe(oldApps[0]); + expect(selectedApp!).toBe(app1); + expect(appsState.snapshot.selectedApp).toBe(app1); }); it('should return null on select when unselecting user', () => { @@ -87,46 +84,46 @@ describe('AppsState', () => { const request = { ...newApp }; appsService.setup(x => x.postApp(request)) - .returns(() => of({ ...newApp, permissions: ['Owner'] })).verifiable(); + .returns(() => of(newApp)).verifiable(); - appsState.create(request, now).subscribe(); + appsState.create(request).subscribe(); - expect(appsState.snapshot.apps.values).toEqual([newApp, ...oldApps]); + expect(appsState.snapshot.apps.values).toEqual([app1, app2, newApp]); }); it('should remove app from snapshot when archived', () => { const request = { ...newApp }; appsService.setup(x => x.postApp(request)) - .returns(() => of({ ...newApp, permissions: ['Owner'] })).verifiable(); + .returns(() => of(newApp)).verifiable(); - appsService.setup(x => x.deleteApp(newApp.name)) + appsService.setup(x => x.deleteApp(newApp)) .returns(() => of({})).verifiable(); - appsState.create(request, now).subscribe(); + appsState.create(request).subscribe(); const appsAfterCreate = appsState.snapshot.apps.values; - appsState.delete(newApp.name).subscribe(); + appsState.delete(newApp).subscribe(); const appsAfterDelete = appsState.snapshot.apps.values; - expect(appsAfterCreate).toEqual([newApp, ...oldApps]); - expect(appsAfterDelete).toEqual(oldApps); + expect(appsAfterCreate).toEqual([app1, app2, newApp]); + expect(appsAfterDelete).toEqual([app1, app2]); }); - it('should selected app from snapshot when archived', () => { + it('should remove selected app from snapshot when archived', () => { const request = { ...newApp }; appsService.setup(x => x.postApp(request)) - .returns(() => of({ ...newApp, permissions: ['Owner'] })).verifiable(); + .returns(() => of(newApp)).verifiable(); - appsService.setup(x => x.deleteApp(newApp.name)) + appsService.setup(x => x.deleteApp(newApp)) .returns(() => of({})).verifiable(); - appsState.create(request, now).subscribe(); + appsState.create(request).subscribe(); appsState.select(newApp.name).subscribe(); - appsState.delete(newApp.name).subscribe(); + appsState.delete(newApp).subscribe(); expect(appsState.snapshot.selectedApp).toBeNull(); }); diff --git a/src/Squidex/app/shared/state/apps.state.ts b/src/Squidex/app/shared/state/apps.state.ts index 32b82d34e..dc29ece97 100644 --- a/src/Squidex/app/shared/state/apps.state.ts +++ b/src/Squidex/app/shared/state/apps.state.ts @@ -10,7 +10,6 @@ import { Observable, of } from 'rxjs'; import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators'; import { - DateTime, DialogService, ImmutableArray, shareSubscribed, @@ -18,7 +17,6 @@ import { } from '@app/framework'; import { - AppCreatedDto, AppDto, AppsService, CreateAppDto @@ -42,6 +40,10 @@ export class AppsState extends State { return this.snapshot.selectedApp ? this.snapshot.selectedApp.name : ''; } + public get selectedAppState() { + return this.snapshot.selectedApp; + } + public selectedApp = this.changes.pipe(map(s => s.selectedApp), distinctUntilChanged(sameApp)); @@ -85,9 +87,8 @@ export class AppsState extends State { shareSubscribed(this.dialogs)); } - public create(request: CreateAppDto, now?: DateTime): Observable { + public create(request: CreateAppDto): Observable { return this.appsService.postApp(request).pipe( - map(payload => createApp(request, payload, now)), tap(created => { this.next(s => { const apps = s.apps.push(created).sortByStringAsc(x => x.name); @@ -98,32 +99,21 @@ export class AppsState extends State { shareSubscribed(this.dialogs, { silent: true })); } - public delete(name: string): Observable { - return this.appsService.deleteApp(name).pipe( + public delete(app: AppDto): Observable { + return this.appsService.deleteApp(app).pipe( tap(() => { this.next(s => { - const apps = s.apps.filter(x => x.name !== name); + const apps = s.apps.filter(x => x.name !== app.name); - const selectedApp = s.selectedApp && s.selectedApp.name === name ? null : s.selectedApp; + const selectedApp = + s.selectedApp && + s.selectedApp.name === app.name ? + null : + s.selectedApp; return { ...s, apps, selectedApp }; }); }), shareSubscribed(this.dialogs)); } -} - -function createApp(request: CreateAppDto, response: AppCreatedDto, now?: DateTime) { - now = now || DateTime.now(); - - const app = new AppDto( - response.id, - request.name, - response.permissions, - now, - now, - response.planName, - response.planUpgrade); - - return app; } \ No newline at end of file diff --git a/src/Squidex/app/shared/state/languages.state.spec.ts b/src/Squidex/app/shared/state/languages.state.spec.ts index 92fc6aa7e..04769a297 100644 --- a/src/Squidex/app/shared/state/languages.state.spec.ts +++ b/src/Squidex/app/shared/state/languages.state.spec.ts @@ -9,6 +9,7 @@ import { of } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; import { + AppLanguagesPayload, AppLanguagesService, DialogService, ImmutableArray, @@ -71,11 +72,11 @@ describe('LanguagesState', () => { expect(languagesState.snapshot.languages.values).toEqual([ { language: oldLanguages.items[0], - fallbackLanguages: ImmutableArray.empty(), - fallbackLanguagesNew: ImmutableArray.of([oldLanguages[1]]) + fallbackLanguages: ImmutableArray.of([oldLanguages.items[1]]), + fallbackLanguagesNew: ImmutableArray.empty() }, { language: oldLanguages.items[1], - fallbackLanguages: ImmutableArray.of([oldLanguages[0]]), + fallbackLanguages: ImmutableArray.of([oldLanguages.items[0]]), fallbackLanguagesNew: ImmutableArray.empty() } ]); @@ -108,15 +109,7 @@ describe('LanguagesState', () => { languagesState.add(languageIT).subscribe(); - expect(languagesState.snapshot.languages.values).toEqual([ - { - language: oldLanguages[0], - fallbackLanguages: ImmutableArray.empty(), - fallbackLanguagesNew: ImmutableArray.empty() - } - ]); - expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageDE, languageIT, languageES]); - expect(languagesState.snapshot.version).toEqual(newVersion); + expectUpdated(updated); }); it('should update language in snapshot when updated', () => { @@ -124,39 +117,35 @@ describe('LanguagesState', () => { const request = { isMaster: true, isOptional: false, fallback: [] }; - languagesService.setup(x => x.putLanguage(app, oldLanguages[1].iso2Code, request, version)) + languagesService.setup(x => x.putLanguage(app, oldLanguages.items[1], request, version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); - languagesState.update(oldLanguages[1], request).subscribe(); + languagesState.update(oldLanguages.items[1], request).subscribe(); - expect(languagesState.snapshot.languages.values).toEqual([ - { - language: oldLanguages[0], - fallbackLanguages: ImmutableArray.empty(), - fallbackLanguagesNew: ImmutableArray.empty() - } - ]); - expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageDE, languageIT, languageES]); - expect(languagesState.snapshot.version).toEqual(newVersion); + expectUpdated(updated); }); it('should remove language from snapshot when deleted', () => { const updated = createLanguages('de'); - languagesService.setup(x => x.deleteLanguage(app, oldLanguages[1].iso2Code, version)) + languagesService.setup(x => x.deleteLanguage(app, oldLanguages.items[1], version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); - languagesState.remove(oldLanguages[1]).subscribe(); + languagesState.remove(oldLanguages.items[1]).subscribe(); + + expectUpdated(updated); + }); + function expectUpdated(updated: AppLanguagesPayload) { expect(languagesState.snapshot.languages.values).toEqual([ { - language: oldLanguages[0], + language: updated.items[0], fallbackLanguages: ImmutableArray.empty(), fallbackLanguagesNew: ImmutableArray.empty() } ]); - expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageDE, languageIT, languageES]); + expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageEN, languageIT, languageES]); expect(languagesState.snapshot.version).toEqual(newVersion); - }); + } }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/languages.state.ts b/src/Squidex/app/shared/state/languages.state.ts index c7ba25e19..00ff0806b 100644 --- a/src/Squidex/app/shared/state/languages.state.ts +++ b/src/Squidex/app/shared/state/languages.state.ts @@ -182,7 +182,7 @@ export class LanguagesState extends State { ImmutableArray.of( language.fallback .map(l => languages.find(x => x.iso2Code === l)).filter(x => !!x) - .map(x => x)), + .map(l => l!)), fallbackLanguagesNew: languages .filter(l => diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.html b/src/Squidex/app/shell/pages/app/left-menu.component.html index 207dd51ee..1d12a0eb3 100644 --- a/src/Squidex/app/shell/pages/app/left-menu.component.html +++ b/src/Squidex/app/shell/pages/app/left-menu.component.html @@ -4,7 +4,7 @@ - -