Browse Source

Backup support for UI settings.

pull/313/head
Sebastian Stehle 8 years ago
parent
commit
92a7f07de7
  1. 111
      src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs
  2. 29
      src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs
  3. 25
      src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs
  4. 2
      src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs
  5. 67
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  6. 6
      src/Squidex/app/shared/components/geolocation-editor.component.ts
  7. 1
      src/Squidex/app/shared/internal.ts
  8. 2
      src/Squidex/app/shared/module.ts
  9. 42
      src/Squidex/app/shared/services/ui.service.spec.ts
  10. 39
      src/Squidex/app/shared/services/ui.service.ts
  11. 102
      src/Squidex/app/shared/state/ui.state.spec.ts
  12. 144
      src/Squidex/app/shared/state/ui.state.ts
  13. 125
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs
  14. 1
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

111
src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs

@ -0,0 +1,111 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Apps
{
public sealed class AppUISettingsGrain : GrainOfGuid, IAppUISettingsGrain
{
private readonly IStore<Guid> store;
private IPersistence<JObject> persistence;
private JObject state = new JObject();
public AppUISettingsGrain(IStore<Guid> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public override Task OnActivateAsync(Guid key)
{
persistence = store.WithSnapshots<JObject, Guid>(GetType(), key, x => state = x);
return persistence.ReadAsync();
}
public Task<J<JObject>> GetAsync()
{
return Task.FromResult(state.AsJ());
}
public Task SetAsync(J<JObject> setting)
{
state = setting;
return persistence.WriteSnapshotAsync(state);
}
public Task SetAsync(string path, J<JToken> value)
{
var container = GetContainer(path, out var key);
if (container == null)
{
throw new InvalidOperationException("Path does not lead to an object.");
}
container[key] = value;
return persistence.WriteSnapshotAsync(state);
}
public Task RemoveAsync(string path)
{
var container = GetContainer(path, out var key);
if (container != null)
{
container.Remove(key);
}
return persistence.WriteSnapshotAsync(state);
}
private JObject GetContainer(string path, out string key)
{
Guard.NotNullOrEmpty(path, nameof(path));
var segments = path.Split('.');
key = segments[segments.Length - 1];
var current = state;
if (segments.Length > 1)
{
foreach (var segment in segments.Take(segments.Length - 1))
{
if (!current.TryGetValue(segment, out var temp))
{
temp = new JObject();
current[segment] = temp;
}
if (temp is JObject next)
{
current = next;
}
else
{
return null;
}
}
}
return current;
}
}
}

29
src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs

@ -26,6 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
public sealed class BackupApps : BackupHandlerWithStore
{
private const string UsersFile = "Users.json";
private const string SettingsFile = "Settings.json";
private readonly IGrainFactory grainFactory;
private readonly IUserResolver userResolver;
private readonly IAppsByNameIndex appsByNameIndex;
@ -69,9 +70,10 @@ namespace Squidex.Domain.Apps.Entities.Apps
}
}
public override Task BackupAsync(Guid appId, BackupWriter writer)
public override async Task BackupAsync(Guid appId, BackupWriter writer)
{
return WriterUsersAsync(writer);
await WriteUsersAsync(writer);
await WriteSettingsAsync(writer, appId);
}
public async override Task RestoreEventAsync(Envelope<IEvent> @event, Guid appId, BackupReader reader, RefToken actor)
@ -120,6 +122,11 @@ namespace Squidex.Domain.Apps.Entities.Apps
}
}
public override Task RestoreAsync(Guid appId, BackupReader reader)
{
return ReadSettingsAsync(reader, appId);
}
private async Task ReserveAppAsync(Guid appId)
{
if (!(isReserved = await appsByNameIndex.ReserveAppAsync(appId, appName)))
@ -167,11 +174,25 @@ namespace Squidex.Domain.Apps.Entities.Apps
usersWithEmail = json.ToObject<Dictionary<string, string>>();
}
private Task WriterUsersAsync(BackupWriter writer)
private async Task WriteUsersAsync(BackupWriter writer)
{
var json = JObject.FromObject(usersWithEmail);
return writer.WriteJsonAsync(UsersFile, json);
await writer.WriteJsonAsync(UsersFile, json);
}
private async Task WriteSettingsAsync(BackupWriter writer, Guid appId)
{
var json = await grainFactory.GetGrain<IAppUISettingsGrain>(appId).GetAsync();
await writer.WriteJsonAsync(SettingsFile, json);
}
private async Task ReadSettingsAsync(BackupReader reader, Guid appId)
{
var json = await reader.ReadJsonAttachmentAsync(SettingsFile);
await grainFactory.GetGrain<IAppUISettingsGrain>(appId).SetAsync((JObject)json);
}
public override async Task CompleteRestoreAsync(Guid appId, BackupReader reader)

25
src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Orleans;
using Squidex.Infrastructure.Orleans;
namespace Squidex.Domain.Apps.Entities.Apps
{
public interface IAppUISettingsGrain : IGrainWithGuidKey
{
Task<J<JObject>> GetAsync();
Task SetAsync(string path, J<JToken> value);
Task SetAsync(J<JObject> setting);
Task RemoveAsync(string path);
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs

@ -6,9 +6,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Templates.Builders;
using Squidex.Domain.Apps.Entities.Schemas.Commands;

67
src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -5,11 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using NSwag.Annotations;
using Orleans;
using Squidex.Areas.Api.Controllers.UI.Models;
using Squidex.Config;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
@ -23,29 +27,74 @@ namespace Squidex.Areas.Api.Controllers.UI
public sealed class UIController : ApiController
{
private readonly MyUIOptions uiOptions;
private readonly IGrainFactory grainFactory;
public UIController(ICommandBus commandBus, IOptions<MyUIOptions> uiOptions)
public UIController(ICommandBus commandBus, IOptions<MyUIOptions> uiOptions, IGrainFactory grainFactory)
: base(commandBus)
{
this.uiOptions = uiOptions.Value;
this.grainFactory = grainFactory;
}
/// <summary>
/// Get ui settings.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => UI settings returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("ui/settings/")]
[Route("apps/{app}/ui/settings/")]
[ProducesResponseType(typeof(UISettingsDto), 200)]
[ApiCosts(0)]
public IActionResult GetSettings()
public async Task<IActionResult> GetSettings(string app)
{
var dto = new UISettingsDto
{
MapType = uiOptions.Map?.Type ?? "OSM",
MapKey = uiOptions.Map?.GoogleMaps?.Key
};
var result = await grainFactory.GetGrain<IAppUISettingsGrain>(App.Id).GetAsync();
return Ok(dto);
result.Value["mapType"] = uiOptions.Map?.Type ?? "OSM";
result.Value["mapKey"] = uiOptions.Map?.GoogleMaps?.Key;
return Ok(result.Value);
}
/// <summary>
/// Set ui settings.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="key">The name of the setting.</param>
/// <param name="value">The name of the value.</param>
/// <returns>
/// 200 => UI setting set.
/// 404 => App not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/ui/settings/{key}")]
[ApiCosts(0)]
public async Task<IActionResult> PutSetting(string app, string key, [FromBody] JToken value)
{
await grainFactory.GetGrain<IAppUISettingsGrain>(App.Id).SetAsync(key, value);
return NoContent();
}
/// <summary>
/// Remove ui settings.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="key">The name of the setting.</param>
/// <returns>
/// 200 => UI setting removed.
/// 404 => App not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/ui/settings/{key}")]
[ApiCosts(0)]
public async Task<IActionResult> DeleteSetting(string app, string key)
{
await grainFactory.GetGrain<IAppUISettingsGrain>(App.Id).RemoveAsync(key);
return NoContent();
}
}
}

6
src/Squidex/app/shared/components/geolocation-editor.component.ts

@ -11,7 +11,7 @@ import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/f
import {
ResourceLoaderService,
Types,
UIService,
UIState,
ValidatorsEx
} from '@app/shared/internal';
@ -72,7 +72,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
constructor(
private readonly resourceLoader: ResourceLoaderService,
private readonly formBuilder: FormBuilder,
private readonly uiService: UIService
private readonly uiState: UIState
) {
}
@ -158,7 +158,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
}
public ngAfterViewInit() {
this.uiService.getSettings()
this.uiState.settings
.subscribe(settings => {
this.isGoogleMaps = settings.mapType === 'GoogleMaps';

1
src/Squidex/app/shared/internal.ts

@ -61,6 +61,7 @@ export * from './state/rule-events.state';
export * from './state/rules.state';
export * from './state/schemas.forms';
export * from './state/schemas.state';
export * from './state/ui.state';
export * from './utils/messages';

2
src/Squidex/app/shared/module.ts

@ -67,6 +67,7 @@ import {
SchemasService,
SchemasState,
UIService,
UIState,
UnsetAppGuard,
UnsetContentGuard,
UsagesService,
@ -179,6 +180,7 @@ export class SqxSharedModule {
SchemasService,
SchemasState,
UIService,
UIState,
UnsetAppGuard,
UnsetContentGuard,
UsagesService,

42
src/Squidex/app/shared/services/ui.service.spec.ts

@ -34,28 +34,22 @@ describe('UIService', () => {
it('should make get request to get settings',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
let settings1: UISettingsDto;
let settings2: UISettingsDto;
let settings: UISettingsDto;
uiService.getSettings().subscribe(result => {
settings1 = result;
uiService.getSettings('my-app').subscribe(result => {
settings = result;
});
const response: UISettingsDto = { mapType: 'OSM', mapKey: '' };
const req = httpMock.expectOne('http://service/p/api/ui/settings');
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(response);
uiService.getSettings().subscribe(result => {
settings2 = result;
});
expect(settings1!).toEqual(response);
expect(settings2!).toEqual(response);
expect(settings!).toEqual(response);
}));
it('should return default settings when error occurs',
@ -63,11 +57,11 @@ describe('UIService', () => {
let settings: UISettingsDto;
uiService.getSettings().subscribe(result => {
uiService.getSettings('my-app').subscribe(result => {
settings = result;
});
const req = httpMock.expectOne('http://service/p/api/ui/settings');
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@ -76,4 +70,26 @@ describe('UIService', () => {
expect(settings!).toBeDefined();
}));
it('should make put request to set value',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
uiService.putSetting('my-app', 'root.nested', 123).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings/root.nested');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
}));
it('should make delete request to remove value',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
uiService.deleteSetting('my-app', 'root.nested').subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings/root.nested');
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toBeNull();
}));
});

39
src/Squidex/app/shared/services/ui.service.ts

@ -8,38 +8,41 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { catchError } from 'rxjs/operators';
import { ApiUrlConfig } from '@app/framework';
export interface UISettingsDto {
mapType: string;
mapKey: string;
mapKey?: string;
}
@Injectable()
export class UIService {
private settings: UISettingsDto;
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public getSettings(): Observable<UISettingsDto> {
if (this.settings) {
return of(this.settings);
} else {
const url = this.apiUrl.buildUrl(`api/ui/settings`);
return this.http.get<UISettingsDto>(url).pipe(
catchError(error => {
return of({ regexSuggestions: [], mapType: 'OSM', mapKey: '' });
}),
tap(settings => {
this.settings = settings;
}));
}
public getSettings(appName: string): Observable<UISettingsDto & object> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings`);
return this.http.get<UISettingsDto>(url).pipe(
catchError(_ => {
return of({ regexSuggestions: [], mapType: 'OSM', mapKey: '' });
}));
}
public putSetting(appName: string, key: string, value: any): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings/${key}`);
return this.http.put(url, value);
}
public deleteSetting(appName: string, key: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings/${key}`);
return this.http.delete(url);
}
}

102
src/Squidex/app/shared/state/ui.state.spec.ts

@ -0,0 +1,102 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { AppsState } from '@app/shared';
import { UIService } from './../services/ui.service';
import { UIState } from './ui.state';
describe('UIState', () => {
const app = 'my-app';
const oldSettings = {
mapType: 'OSM'
};
let appsState: IMock<AppsState>;
let uiService: IMock<UIService>;
let uiState: UIState;
beforeEach(() => {
appsState = Mock.ofType<AppsState>();
appsState.setup(x => x.appName)
.returns(() => app);
uiService = Mock.ofType<UIService>();
uiService.setup(x => x.getSettings(app))
.returns(() => of(oldSettings));
uiService.setup(x => x.putSetting(app, It.isAnyString(), It.isAny()))
.returns(() => of({}));
uiService.setup(x => x.deleteSetting(app, It.isAnyString()))
.returns(() => of({}));
uiState = new UIState(appsState.object, uiService.object);
});
it('should load settings', () => {
expect(uiState.snapshot.settings).toEqual(oldSettings);
});
it('should add value to snapshot when set', () => {
uiState.set('root.nested', 123);
expect(uiState.snapshot.settings).toEqual({
mapType: 'OSM',
root: {
nested: 123
}
});
uiState.get('root', {}).subscribe(x => {
expect(x).toEqual({ nested: 123 });
});
uiState.get('root.nested', 0).subscribe(x => {
expect(x).toEqual(123);
});
uiState.get('root.notfound', 1337).subscribe(x => {
expect(x).toEqual(1337);
});
uiService.verify(x => x.putSetting(app, 'root.nested', 123), Times.once());
});
it('should remove value from snapshot when removed', () => {
uiState.set('root.nested1', 123);
uiState.set('root.nested2', 123);
uiState.remove('root.nested1');
expect(uiState.snapshot.settings).toEqual({
mapType: 'OSM',
root: {
nested2: 123
}
});
uiState.get('root', {}).subscribe(x => {
expect(x).toEqual({ nested2: 123 });
});
uiState.get('root.nested2', 0).subscribe(x => {
expect(x).toEqual(123);
});
uiState.get('root.nested1', 1337).subscribe(x => {
expect(x).toEqual(1337);
});
uiService.verify(x => x.deleteSetting(app, 'root.nested1'), Times.once());
});
});

144
src/Squidex/app/shared/state/ui.state.ts

@ -0,0 +1,144 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { State, Types } from '@app/framework';
import { AppsState } from './apps.state';
import { UIService, UISettingsDto } from './../services/ui.service';
interface Snapshot {
settings: object & any;
}
@Injectable()
export class UIState extends State<Snapshot> {
public settings =
this.changes.pipe(map(x => x.settings),
distinctUntilChanged());
public get<T>(path: string, defaultValue: T) {
return this.settings.pipe(map(x => this.getValue(x, path, defaultValue)),
distinctUntilChanged());
}
constructor(
private readonly appsState: AppsState,
private readonly uiService: UIService
) {
super({ settings: { mapType: 'OSM' } });
if (appsState.selectedApp && Types.isFunction(appsState.selectedApp.subscribe)) {
appsState.selectedApp.subscribe(app => {
if (app) {
this.load(true);
}
});
} else {
this.load(true);
}
}
public load(reset = false): Observable<any> {
if (!reset) {
this.resetState();
}
return this.loadInternal();
}
private loadInternal(): Observable<any> {
return this.uiService.getSettings(this.appName).pipe(
tap(dtos => {
return this.next({ settings: dtos });
}));
}
public set(path: string, value: any) {
const { key, current, root } = this.getContainer(path);
if (current && key) {
this.uiService.putSetting(this.appName, path, value).subscribe();
current[key] = value;
this.next({ settings: root });
}
}
public remove(path: string) {
const { key, current, root } = this.getContainer(path);
if (current && key) {
this.uiService.deleteSetting(this.appName, path).subscribe();
delete current[key];
this.next({ settings: root });
}
}
private getContainer(path: string) {
const segments = path.split('.');
let current = { ...this.snapshot.settings };
const root = current;
if (segments.length > 0) {
for (let i = 0; i < segments.length - 1; i++) {
const segment = segments[i];
let temp = current[segment];
if (!temp) {
temp = {};
} else {
temp = { ...temp };
}
current[segment] = temp;
if (!Types.isObject(temp)) {
return { key: null, current: null, root: null };
}
current = temp;
}
}
return { key: segments[segments.length - 1], current, root };
}
private getValue<T>(setting: object & UISettingsDto, path: string, defaultValue: T) {
const segments = path.split('.');
let current = setting;
for (let segment of segments) {
let temp = current[segment];
if (temp) {
current[segment] = temp;
} else {
return defaultValue;
}
current = temp;
}
return <T><any>current;
}
private get appName() {
return this.appsState.appName;
}
}

125
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs

@ -0,0 +1,125 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Apps
{
public sealed class AppUISettingsGrainTests
{
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>();
private readonly IPersistence<JObject> persistence = A.Fake<IPersistence<JObject>>();
private readonly AppUISettingsGrain sut;
public AppUISettingsGrainTests()
{
A.CallTo(() => store.WithSnapshots(A<Type>.Ignored, A<Guid>.Ignored, A<Func<JObject, Task>>.Ignored))
.Returns(persistence);
sut = new AppUISettingsGrain(store);
sut.OnActivateAsync(Guid.Empty).Wait();
}
[Fact]
public async Task Should_set_setting()
{
await sut.SetAsync(new JObject(new JProperty("key", 15)).AsJ());
var actual = await sut.GetAsync();
var expected =
new JObject(
new JProperty("key", 15));
Assert.Equal(expected.ToString(), actual.Value.ToString());
}
[Fact]
public async Task Should_set_root_value()
{
await sut.SetAsync("key", ((JToken)123).AsJ());
var actual = await sut.GetAsync();
var expected =
new JObject(
new JProperty("key", 123));
Assert.Equal(expected.ToString(), actual.Value.ToString());
}
[Fact]
public async Task Should_remove_root_value()
{
await sut.SetAsync("key", ((JToken)123).AsJ());
await sut.RemoveAsync("key");
var actual = await sut.GetAsync();
var expected = new JObject();
Assert.Equal(expected.ToString(), actual.Value.ToString());
}
[Fact]
public async Task Should_set_nested_value()
{
await sut.SetAsync("root.nested", ((JToken)123).AsJ());
var actual = await sut.GetAsync();
var expected =
new JObject(
new JProperty("root",
new JObject(
new JProperty("nested", 123))));
Assert.Equal(expected.ToString(), actual.Value.ToString());
}
[Fact]
public async Task Should_remove_nested_value()
{
await sut.SetAsync("root.nested", ((JToken)123).AsJ());
await sut.RemoveAsync("root.nested");
var actual = await sut.GetAsync();
var expected =
new JObject(
new JProperty("root", new JObject()));
Assert.Equal(expected.ToString(), actual.Value.ToString());
}
[Fact]
public async Task Should_throw_exception_if_nested_not_an_object()
{
await sut.SetAsync("root.nested", ((JToken)123).AsJ());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.SetAsync("root.nested.value", ((JToken)123).AsJ()));
}
[Fact]
public Task Should_do_nothing_if_deleting_and_nested_not_found()
{
return sut.RemoveAsync("root.nested");
}
[Fact]
public Task Should_do_nothing_if_deleting_and_key_not_found()
{
return sut.RemoveAsync("root");
}
}
}

1
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -13,7 +13,6 @@ using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using NodaTime.Extensions;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;

Loading…
Cancel
Save