Browse Source

Some progress

pull/1/head
Sebastian 9 years ago
parent
commit
182e825bbd
  1. 2
      src/Squidex.Events/Apps/AppClientAttached.cs
  2. 21
      src/Squidex.Events/Apps/AppClientRenamed.cs
  3. 5
      src/Squidex.Events/Apps/AppClientRevoked.cs
  4. 163
      src/Squidex.Infrastructure/CQRS/EventStore/EventStoreBus.cs
  5. 4
      src/Squidex.Infrastructure/CQRS/EventStore/IStreamPositionStorage.cs
  6. 19
      src/Squidex.Infrastructure/InfrastructureErrors.cs
  7. 4
      src/Squidex.Read/Apps/IAppClientEntity.cs
  8. 1
      src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs
  9. 6
      src/Squidex.Store.MongoDb/Apps/MongoAppClientEntity.cs
  10. 12
      src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs
  11. 47
      src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs
  12. 3
      src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionEntity.cs
  13. 24
      src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionStorage.cs
  14. 20
      src/Squidex.Write/Apps/AppClient.cs
  15. 7
      src/Squidex.Write/Apps/AppCommandHandler.cs
  16. 45
      src/Squidex.Write/Apps/AppDomainObject.cs
  17. 6
      src/Squidex.Write/Apps/Commands/AttachClient.cs
  18. 33
      src/Squidex.Write/Apps/Commands/RenameClient.cs
  19. 6
      src/Squidex.Write/Apps/Commands/RevokeClient.cs
  20. 5
      src/Squidex/.sass-lint.yml
  21. 4
      src/Squidex/Config/Domain/InfrastructureModule.cs
  22. 2
      src/Squidex/Config/Identity/LazyClientStore.cs
  23. 25
      src/Squidex/Controllers/Api/Apps/AppClientsController.cs
  24. 4
      src/Squidex/Controllers/Api/Apps/Models/AttachClientDto.cs
  25. 10
      src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs
  26. 22
      src/Squidex/Controllers/Api/Apps/Models/RenameClientDto.cs
  27. 2
      src/Squidex/Pipeline/AppFilterAttribute.cs
  28. BIN
      src/Squidex/app-libs/icomoon/fonts/icomoon.eot
  29. 1
      src/Squidex/app-libs/icomoon/fonts/icomoon.svg
  30. BIN
      src/Squidex/app-libs/icomoon/fonts/icomoon.ttf
  31. BIN
      src/Squidex/app-libs/icomoon/fonts/icomoon.woff
  32. 33
      src/Squidex/app-libs/icomoon/selection.json
  33. 13
      src/Squidex/app-libs/icomoon/style.css
  34. 104
      src/Squidex/app/components/internal/app/settings/contributors-page.component.html
  35. 4
      src/Squidex/app/components/internal/app/settings/contributors-page.component.scss
  36. 47
      src/Squidex/app/theme/_bootstrap.scss
  37. 2
      src/Squidex/app/theme/_layout.scss
  38. 1
      src/Squidex/app/theme/_vars.scss
  39. 2
      src/Squidex/appsettings.json
  40. 21
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs
  41. 68
      tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs

2
src/Squidex.Events/Apps/AppClientAttached.cs

@ -15,7 +15,7 @@ namespace Squidex.Events.Apps
[TypeName("AppClientAttachedEvent")]
public sealed class AppClientAttached : IEvent
{
public string ClientName { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }

21
src/Squidex.Events/Apps/AppClientRenamed.cs

@ -0,0 +1,21 @@
// ==========================================================================
// AppClientRenamed.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Events.Apps
{
[TypeName("AppClientRenamedEvent")]
public sealed class AppClientRenamed : IEvent
{
public string ClientId { get; set; }
public string Name { get; set; }
}
}

5
src/Squidex.Events/Apps/AppClientRevoked.cs

@ -6,7 +6,6 @@
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
@ -15,8 +14,6 @@ namespace Squidex.Events.Apps
[TypeName("AppClientRevokedEvent")]
public sealed class AppClientRevoked : IEvent
{
public string ClientName { get; set; }
public DateTime ExpiresUtc { get; set; }
public string ClientId { get; set; }
}
}

163
src/Squidex.Infrastructure/CQRS/EventStore/EventStoreBus.cs

@ -9,15 +9,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using EventStore.ClientAPI;
using EventStore.ClientAPI.SystemData;
using Microsoft.Extensions.Logging;
using Squidex.Infrastructure.CQRS.Events;
// ReSharper disable InvertIf
namespace Squidex.Infrastructure.CQRS.EventStore
{
public sealed class EventStoreBus
public sealed class EventStoreBus : IDisposable
{
private readonly IEventStoreConnection connection;
private readonly UserCredentials credentials;
@ -26,8 +28,9 @@ namespace Squidex.Infrastructure.CQRS.EventStore
private readonly IEnumerable<ICatchEventConsumer> catchConsumers;
private readonly ILogger<EventStoreBus> logger;
private readonly IStreamPositionStorage positions;
private EventStoreCatchUpSubscription catchSubscription;
private bool isLive;
private readonly List<EventStoreCatchUpSubscription> catchSubscriptions = new List<EventStoreCatchUpSubscription>();
private EventStoreSubscription liveSubscription;
private bool isSubscribed;
public EventStoreBus(
ILogger<EventStoreBus> logger,
@ -55,18 +58,62 @@ namespace Squidex.Infrastructure.CQRS.EventStore
this.catchConsumers = catchConsumers;
}
public void Dispose()
{
lock (catchSubscriptions)
{
foreach (var catchSubscription in catchSubscriptions)
{
catchSubscription.Stop(TimeSpan.FromMinutes(1));
}
liveSubscription.Unsubscribe();
}
}
public void Subscribe(string streamName = "$all")
{
Guard.NotNullOrEmpty(streamName, nameof(streamName));
if (catchSubscription != null)
if (isSubscribed)
{
return;
}
var position = positions.ReadPosition();
logger.LogInformation("Subscribing from {0}", position.HasValue ? position.Value.ToString() : "beginning");
SubscribeLive(streamName);
SubscribeCatch(streamName);
isSubscribed = true;
}
private void SubscribeLive(string streamName)
{
Task.Run(async () =>
{
liveSubscription =
await connection.SubscribeToStreamAsync(streamName, true,
(subscription, resolvedEvent) =>
{
OnLiveEvent(streamName, resolvedEvent);
}, userCredentials: credentials);
}).Wait();
}
private void SubscribeCatch(string streamName)
{
foreach (var catchConsumer in catchConsumers)
{
SubscribeCatchFor(catchConsumer, streamName);
}
}
private void SubscribeCatchFor(IEventConsumer consumer, string streamName)
{
var subscriptionName = consumer.GetType().GetTypeInfo().Name;
var position = positions.ReadPosition(subscriptionName);
logger.LogInformation("[{0}]: Subscribing from {0}", consumer, position ?? 0);
var settings =
new CatchUpSubscriptionSettings(
@ -74,47 +121,92 @@ namespace Squidex.Infrastructure.CQRS.EventStore
true,
true);
catchSubscription =
var catchSubscription =
connection.SubscribeToStreamFrom(streamName, position, settings,
OnEvent,
OnLiveProcessingStarted,
userCredentials: credentials);
(subscription, resolvedEvent) =>
{
OnCatchEvent(consumer, streamName, resolvedEvent, subscriptionName, subscription);
}, userCredentials: credentials);
lock (catchSubscriptions)
{
catchSubscriptions.Add(catchSubscription);
}
}
private void OnEvent(EventStoreCatchUpSubscription subscription, ResolvedEvent resolvedEvent)
private void OnLiveEvent(string streamName, ResolvedEvent resolvedEvent)
{
Envelope<IEvent> @event = null;
try
{
if (resolvedEvent.OriginalEvent.EventStreamId.StartsWith("$", StringComparison.OrdinalIgnoreCase))
{
return;
}
@event = formatter.Parse(new EventWrapper(resolvedEvent));
}
catch (Exception ex)
{
logger.LogError(InfrastructureErrors.EventDeserializationFailed, ex,
"[LiveConsumers]: Failed to deserialize event {0}#{1}", streamName,
resolvedEvent.OriginalEventNumber);
}
if (!liveConsumers.Any() && !catchConsumers.Any())
{
return;
}
if (@event != null)
{
DispatchConsumers(liveConsumers, @event).Wait();
}
}
var @event = formatter.Parse(new EventWrapper(resolvedEvent));
private void OnCatchEvent(IEventConsumer consumer, string streamName, ResolvedEvent resolvedEvent, string subscriptionName, EventStoreCatchUpSubscription subscription)
{
if (resolvedEvent.OriginalEvent.EventStreamId.StartsWith("$", StringComparison.OrdinalIgnoreCase))
{
return;
}
logger.LogInformation("Received event {0} ({1})", @event.Payload.GetType().Name, @event.Headers.AggregateId());
var isFailed = false;
if (isLive)
{
DispatchConsumers(liveConsumers, @event).Wait();
}
Envelope<IEvent> @event = null;
DispatchConsumers(catchConsumers, @event).Wait();
try
{
@event = formatter.Parse(new EventWrapper(resolvedEvent));
}
finally
catch (Exception ex)
{
positions.WritePosition(resolvedEvent.OriginalEventNumber);
logger.LogError(InfrastructureErrors.EventDeserializationFailed, ex,
"[{consumer}]: Failed to deserialize event {1}#{2}", consumer, streamName,
resolvedEvent.OriginalEventNumber);
isFailed = true;
}
}
private void OnLiveProcessingStarted(EventStoreCatchUpSubscription subscription)
{
isLive = true;
if (@event != null)
{
try
{
logger.LogInformation("Received event {0} ({1})", @event.Payload.GetType().Name, @event.Headers.AggregateId());
consumer.On(@event).Wait();
positions.WritePosition(subscriptionName, resolvedEvent.OriginalEventNumber);
}
catch (Exception ex)
{
logger.LogError(InfrastructureErrors.EventHandlingFailed, ex,
"[{0}]: Failed to handle event {1} ({2})", consumer,
@event.Payload,
@event.Headers.EventId());
}
}
if (isFailed)
{
lock (catchSubscriptions)
{
subscription.Stop();
catchSubscriptions.Remove(subscription);
}
}
}
private Task DispatchConsumers(IEnumerable<IEventConsumer> consumers, Envelope<IEvent> @event)
@ -130,9 +222,8 @@ namespace Squidex.Infrastructure.CQRS.EventStore
}
catch (Exception ex)
{
var eventId = new EventId(10001, "EventConsumeFailed");
logger.LogError(eventId, ex, "'{0}' failed to handle event {1} ({2})", consumer, @event.Payload, @event.Headers.EventId());
logger.LogError(InfrastructureErrors.EventHandlingFailed, ex,
"[{0}]: Failed to handle event {1} ({2})", consumer, @event.Payload, @event.Headers.EventId());
}
}
}

4
src/Squidex.Infrastructure/CQRS/EventStore/IStreamPositionStorage.cs

@ -9,8 +9,8 @@ namespace Squidex.Infrastructure.CQRS.EventStore
{
public interface IStreamPositionStorage
{
int? ReadPosition();
int? ReadPosition(string subscriptionName);
void WritePosition(int position);
void WritePosition(string subscriptionName, int position);
}
}

19
src/Squidex.Infrastructure/InfrastructureErrors.cs

@ -0,0 +1,19 @@
// ==========================================================================
// InfrastructureErrors.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.Logging;
namespace Squidex.Infrastructure
{
public class InfrastructureErrors
{
public static readonly EventId EventHandlingFailed = new EventId(10001, "EventHandlingFailed");
public static readonly EventId EventDeserializationFailed = new EventId(10002, "EventDeserializationFailed");
}
}

4
src/Squidex.Read/Apps/IAppClientEntity.cs

@ -12,7 +12,9 @@ namespace Squidex.Read.Apps
{
public interface IAppClientEntity
{
string ClientName { get; }
string Name { get; }
string ClientId { get; }
string ClientSecret { get; }

1
src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs

@ -68,6 +68,7 @@ namespace Squidex.Read.Apps.Services.Implementations
@event.Payload is AppContributorRemoved ||
@event.Payload is AppClientAttached ||
@event.Payload is AppClientRevoked ||
@event.Payload is AppClientRenamed ||
@event.Payload is AppLanguagesConfigured)
{
var appName = Cache.Get<string>(BuildNamesCacheKey(@event.Headers.AggregateId()));

6
src/Squidex.Store.MongoDb/Apps/MongoAppClientEntity.cs

@ -16,7 +16,7 @@ namespace Squidex.Store.MongoDb.Apps
{
[BsonRequired]
[BsonElement]
public string ClientName { get; set; }
public string ClientId { get; set; }
[BsonRequired]
[BsonElement]
@ -25,5 +25,9 @@ namespace Squidex.Store.MongoDb.Apps
[BsonRequired]
[BsonElement]
public DateTime ExpiresUtc { get; set; }
[BsonRequired]
[BsonElement]
public string Name { get; set; }
}
}

12
src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs

@ -27,11 +27,11 @@ namespace Squidex.Store.MongoDb.Apps
[BsonRequired]
[BsonElement]
public List<MongoAppClientEntity> Clients { get; set; }
public Dictionary<string, MongoAppClientEntity> Clients { get; set; }
[BsonRequired]
[BsonElement]
public List<MongoAppContributorEntity> Contributors { get; set; }
public Dictionary<string, MongoAppContributorEntity> Contributors { get; set; }
IEnumerable<Language> IAppEntity.Languages
{
@ -40,19 +40,19 @@ namespace Squidex.Store.MongoDb.Apps
IEnumerable<IAppClientEntity> IAppEntity.Clients
{
get { return Clients; }
get { return Clients.Values; }
}
IEnumerable<IAppContributorEntity> IAppEntity.Contributors
{
get { return Contributors; }
get { return Contributors.Values; }
}
public MongoAppEntity()
{
Contributors = new List<MongoAppContributorEntity>();
Contributors = new Dictionary<string, MongoAppContributorEntity>();
Clients = new List<MongoAppClientEntity>();
Clients = new Dictionary<string, MongoAppClientEntity>();
}
}
}

47
src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs

@ -18,6 +18,7 @@ using Squidex.Infrastructure.Reflection;
using Squidex.Read.Apps;
using Squidex.Read.Apps.Repositories;
using Squidex.Store.MongoDb.Utils;
using Squidex.Infrastructure;
namespace Squidex.Store.MongoDb.Apps
{
@ -41,7 +42,7 @@ namespace Squidex.Store.MongoDb.Apps
public async Task<IReadOnlyList<IAppEntity>> QueryAllAsync(string subjectId)
{
var entities =
await Collection.Find(s => s.Contributors.Any(c => c.ContributorId == subjectId)).ToListAsync();
await Collection.Find(s => s.Contributors.ContainsKey(subjectId)).ToListAsync();
return entities;
}
@ -56,43 +57,59 @@ namespace Squidex.Store.MongoDb.Apps
public Task On(AppCreated @event, EnvelopeHeaders headers)
{
return Collection.CreateAsync(headers, a => SimpleMapper.Map(@event, a));
return Collection.CreateAsync(headers, a =>
{
SimpleMapper.Map(@event, a);
});
}
public Task On(AppContributorRemoved @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a => a.Contributors.RemoveAll(c => c.ContributorId == @event.ContributorId));
return Collection.UpdateAsync(headers, a =>
{
a.Contributors.Remove(@event.ContributorId);
});
}
public Task On(AppLanguagesConfigured @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a => a.Languages = @event.Languages.Select(x => x.Iso2Code).ToList());
return Collection.UpdateAsync(headers, a =>
{
a.Languages = @event.Languages.Select(x => x.Iso2Code).ToList();
});
}
public Task On(AppClientAttached @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a => a.Clients.Add(SimpleMapper.Map(@event, new MongoAppClientEntity())));
return Collection.UpdateAsync(headers, a =>
{
a.Clients.Add(@event.ClientId, SimpleMapper.Map(@event, new MongoAppClientEntity()));
});
}
public Task On(AppClientRevoked @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a => a.Clients.RemoveAll(c => c.ClientName == @event.ClientName));
return Collection.UpdateAsync(headers, a =>
{
a.Clients.Remove(@event.ClientId);
});
}
public Task On(AppContributorAssigned @event, EnvelopeHeaders headers)
public Task On(AppClientRenamed @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
var contributor = a.Contributors.Find(x => x.ContributorId == @event.ContributorId);
if (contributor == null)
{
contributor = new MongoAppContributorEntity { ContributorId = @event.ContributorId };
a.Clients[@event.ClientId].Name = @event.Name;
});
}
a.Contributors.Add(contributor);
}
public Task On(AppContributorAssigned @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
var contributor = a.Contributors.GetOrAddNew(@event.ContributorId);
contributor.Permission = @event.Permission;
SimpleMapper.Map(@event, contributor);
});
}

3
src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionEntity.cs

@ -18,6 +18,9 @@ namespace Squidex.Store.MongoDb.Infrastructure
[BsonId]
public ObjectId Id { get; set; }
[BsonElement]
public string SubscriptionName { get; set; }
[BsonElement]
public int? Position { get; set; }
}

24
src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionStorage.cs

@ -6,8 +6,9 @@
// All rights reserved.
// ==========================================================================
using MongoDB.Bson;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.EventStore;
using Squidex.Store.MongoDb.Utils;
@ -17,25 +18,30 @@ namespace Squidex.Store.MongoDb.Infrastructure
{
public sealed class MongoStreamPositionStorage : MongoRepositoryBase<MongoStreamPositionEntity>, IStreamPositionStorage
{
private static readonly ObjectId Id = new ObjectId("507f1f77bcf86cd799439011");
public MongoStreamPositionStorage(IMongoDatabase database)
: base(database)
{
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoStreamPositionEntity> collection)
{
return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.SubscriptionName), new CreateIndexOptions { Unique = true });
}
protected override string CollectionName()
{
return "StreamPositions";
}
public int? ReadPosition()
public int? ReadPosition(string subscriptionName)
{
var document = Collection.Find(t => t.Id == Id).FirstOrDefault();
Guard.NotNullOrEmpty(subscriptionName, nameof(subscriptionName));
var document = Collection.Find(t => t.SubscriptionName == subscriptionName).FirstOrDefault();
if (document == null)
{
document = new MongoStreamPositionEntity { Id = Id };
document = new MongoStreamPositionEntity { SubscriptionName = subscriptionName };
Collection.InsertOne(document);
}
@ -43,9 +49,11 @@ namespace Squidex.Store.MongoDb.Infrastructure
return document.Position;
}
public void WritePosition(int position)
public void WritePosition(string subscriptionName, int position)
{
Collection.UpdateOne(t => t.Id == Id, Update.Set(t => t.Position, position));
Guard.NotNullOrEmpty(subscriptionName, nameof(subscriptionName));
Collection.UpdateOne(t => t.SubscriptionName == subscriptionName, Update.Set(t => t.Position, position));
}
}
}

20
src/Squidex.Write/Apps/AppClient.cs

@ -13,21 +13,33 @@ namespace Squidex.Write.Apps
{
public sealed class AppClient
{
public string ClientName { get; }
private string name;
public string ClientId { get; }
public string ClientSecret { get; }
public DateTime ExpiresUtc { get; }
public AppClient(string name, string secret, DateTime expiresUtc)
public string Name
{
get { return name ?? ClientId; }
}
public AppClient(string id, string secret, DateTime expiresUtc)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNullOrEmpty(id, nameof(id));
Guard.NotNullOrEmpty(secret, nameof(secret));
ClientName = name;
ClientId = id;
ClientSecret = secret;
ExpiresUtc = expiresUtc;
}
public void Rename(string newName)
{
name = newName;
}
}
}

7
src/Squidex.Write/Apps/AppCommandHandler.cs

@ -82,7 +82,7 @@ namespace Squidex.Write.Apps
{
x.AttachClient(command, keyGenerator.GenerateKey());
context.Succeed(x.Clients[command.ClientName]);
context.Succeed(x.Clients[command.ClientId]);
});
}
@ -91,6 +91,11 @@ namespace Squidex.Write.Apps
return handler.UpdateAsync<AppDomainObject>(command, x => x.RemoveContributor(command));
}
protected Task On(RenameClient command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(command, x => x.RenameClient(command));
}
protected Task On(RevokeClient command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(command, x => x.RevokeClient(command));

45
src/Squidex.Write/Apps/AppDomainObject.cs

@ -66,12 +66,17 @@ namespace Squidex.Write.Apps
public void On(AppClientAttached @event)
{
clients.Add(@event.ClientName, new AppClient(@event.ClientName, @event.ClientSecret, @event.ExpiresUtc));
clients.Add(@event.ClientId, new AppClient(@event.ClientId, @event.ClientSecret, @event.ExpiresUtc));
}
public void On(AppClientRevoked @event)
{
clients.Remove(@event.ClientName);
clients.Remove(@event.ClientId);
}
public void On(AppClientRenamed @event)
{
clients[@event.ClientId].Rename(@event.Name);
}
protected override void DispatchEvent(Envelope<IEvent> @event)
@ -109,14 +114,28 @@ namespace Squidex.Write.Apps
return this;
}
public AppDomainObject RenameClient(RenameClient command)
{
Func<string> message = () => "Cannot rename client";
Guard.Valid(command, nameof(command), message);
ThrowIfNotCreated();
ThrowIfClientNotFound(command.ClientId, message);
RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed()));
return this;
}
public AppDomainObject RevokeClient(RevokeClient command)
{
Func<string> message = () => "Cannot revoke client";
Guard.Valid(command, nameof(command), () => "Cannot revoke client");
Guard.Valid(command, nameof(command), message);
ThrowIfNotCreated();
ThrowIfClientNotFound(command, message);
ThrowIfClientNotFound(command.ClientId, message);
RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked()));
@ -130,7 +149,7 @@ namespace Squidex.Write.Apps
Guard.Valid(command, nameof(command), () => "Cannot attach client");
ThrowIfNotCreated();
ThrowIfClientFound(command, message);
ThrowIfClientFound(command.ClientId, message);
var expire = command.Timestamp.AddYears(1);
@ -146,7 +165,7 @@ namespace Squidex.Write.Apps
Guard.Valid(command, nameof(command), () => "Cannot remove contributor");
ThrowIfNotCreated();
ThrowIfContributorNotFound(command, message);
ThrowIfContributorNotFound(command.ContributorId, message);
ThrowIfNoOwner(c => c.Remove(command.ContributorId), message);
@ -194,19 +213,19 @@ namespace Squidex.Write.Apps
}
}
private void ThrowIfClientFound(AttachClient command, Func<string> message)
private void ThrowIfClientFound(string clientId, Func<string> message)
{
if (clients.ContainsKey(command.ClientName))
if (clients.ContainsKey(clientId))
{
var error = new ValidationError("Client name is alreay part of the app", "ClientName");
var error = new ValidationError("Client id is alreay part of the app", "ClientName");
throw new ValidationException(message(), error);
}
}
private void ThrowIfClientNotFound(RevokeClient command, Func<string> message)
private void ThrowIfClientNotFound(string clientId, Func<string> message)
{
if (!clients.ContainsKey(command.ClientName))
if (!clients.ContainsKey(clientId))
{
var error = new ValidationError("Client is not part of the app", "ClientName");
@ -214,9 +233,9 @@ namespace Squidex.Write.Apps
}
}
private void ThrowIfContributorNotFound(RemoveContributor command, Func<string> message)
private void ThrowIfContributorNotFound(string contributorId, Func<string> message)
{
if (!contributors.ContainsKey(command.ContributorId))
if (!contributors.ContainsKey(contributorId))
{
var error = new ValidationError("Contributor is not part of the app", "ContributorId");

6
src/Squidex.Write/Apps/Commands/AttachClient.cs

@ -15,15 +15,15 @@ namespace Squidex.Write.Apps.Commands
{
public sealed class AttachClient : AppAggregateCommand, ITimestampCommand, IValidatable
{
public string ClientName { get; set; }
public string ClientId { get; set; }
public DateTime Timestamp { get; set; }
public void Validate(IList<ValidationError> errors)
{
if (!ClientName.IsSlug())
if (!ClientId.IsSlug())
{
errors.Add(new ValidationError("Name must be a valid slug", nameof(ClientName)));
errors.Add(new ValidationError("Client id must be a valid slug", nameof(ClientId)));
}
}
}

33
src/Squidex.Write/Apps/Commands/RenameClient.cs

@ -0,0 +1,33 @@
// ==========================================================================
// RenameClient.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Write.Apps.Commands
{
public class RenameClient : AppAggregateCommand, IValidatable
{
public string ClientId { get; set; }
public string Name { get; set; }
public void Validate(IList<ValidationError> errors)
{
if (string.IsNullOrWhiteSpace(Name))
{
errors.Add(new ValidationError("Name cannot be null or empty", nameof(Name)));
}
if (!ClientId.IsSlug())
{
errors.Add(new ValidationError("Client id must be a valid slug", nameof(ClientId)));
}
}
}
}

6
src/Squidex.Write/Apps/Commands/RevokeClient.cs

@ -13,13 +13,13 @@ namespace Squidex.Write.Apps.Commands
{
public class RevokeClient : AppAggregateCommand, IValidatable
{
public string ClientName { get; set; }
public string ClientId { get; set; }
public void Validate(IList<ValidationError> errors)
{
if (!ClientName.IsSlug())
if (!ClientId.IsSlug())
{
errors.Add(new ValidationError("Name must be a valid slug", nameof(ClientName)));
errors.Add(new ValidationError("Client id must be a valid slug", nameof(ClientId)));
}
}
}

5
src/Squidex/.sass-lint.yml

@ -12,6 +12,11 @@ rules:
-
size: 4
nesting-depth:
- 1
-
max-depth: 4
leading-underscore: false
files:

4
src/Squidex/Config/Domain/InfrastructureModule.cs

@ -42,6 +42,10 @@ namespace Squidex.Config.Domain
.As<IDomainObjectRepository>()
.SingleInstance();
builder.RegisterType<AggregateHandler>()
.As<IAggregateHandler>()
.SingleInstance();
builder.RegisterType<InMemoryCommandBus>()
.As<ICommandBus>()
.SingleInstance();

2
src/Squidex/Config/Identity/LazyClientStore.cs

@ -52,7 +52,7 @@ namespace Squidex.Config.Identity
var app = await appProvider.FindAppByNameAsync(token[0]);
var appClient = app?.Clients.FirstOrDefault(x => x.ClientName == token[1]);
var appClient = app?.Clients.FirstOrDefault(x => x.ClientId == token[1]);
if (appClient == null)
{

25
src/Squidex/Controllers/Api/Apps/AppClientsController.cs

@ -92,19 +92,40 @@ namespace Squidex.Controllers.Api.Apps
return StatusCode(201, response);
}
/// <summary>
/// Updates an app client.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="client">The id of the client that must be updated.</param>
/// <param name="request">Client object that needs to be added to the app.</param>
/// <returns>
/// 201 => Client key generated.
/// 404 => App not found or client not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/clients/{client}/")]
[ProducesResponseType(typeof(ClientDto[]), 201)]
public async Task<IActionResult> PutClient(string app, string client, [FromBody] RenameClientDto request)
{
await CommandBus.PublishAsync(SimpleMapper.Map(request, new RenameClient()));
return NoContent();
}
/// <summary>
/// Revoke an app client
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="client">Client object that needs to be added to the app.</param>
/// <param name="client">The id of the client that must be deleted.</param>
/// <returns>
/// 404 => App not found or client not found.
/// 204 => Client revoked.
/// </returns>
[HttpDelete]
[Route("apps/{app}/clients/{client}/")]
public async Task<IActionResult> DeleteClient(string app, string client)
{
await CommandBus.PublishAsync(new RevokeClient { ClientName = client });
await CommandBus.PublishAsync(new RevokeClient { ClientId = client });
return NoContent();
}

4
src/Squidex/Controllers/Api/Apps/Models/AttachClientDto.cs

@ -13,10 +13,10 @@ namespace Squidex.Controllers.Api.Apps.Models
public sealed class AttachClientDto
{
/// <summary>
/// The name of the client.
/// The id of the client.
/// </summary>
[Required]
[RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
public string ClientName { get; set; }
public string ClientId { get; set; }
}
}

10
src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs

@ -14,10 +14,10 @@ namespace Squidex.Controllers.Api.Apps.Models
public sealed class ClientDto
{
/// <summary>
/// The client name.
/// The client id.
/// </summary>
[Required]
public string ClientName { get; set; }
public string ClientId { get; set; }
/// <summary>
/// The client secret.
@ -30,5 +30,11 @@ namespace Squidex.Controllers.Api.Apps.Models
/// </summary>
[Required]
public DateTime ExpiresUtc { get; set; }
/// <summary>
/// The client name.
/// </summary>
[Required]
public string Name { get; set; }
}
}

22
src/Squidex/Controllers/Api/Apps/Models/RenameClientDto.cs

@ -0,0 +1,22 @@
// ==========================================================================
// RenameClientDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Apps.Models
{
public class RenameClientDto
{
/// <summary>
/// The new display name of the client.
/// </summary>
[Required]
[StringLength(20)]
public string Name { get; set; }
}
}

2
src/Squidex/Pipeline/AppFilterAttribute.cs

@ -77,7 +77,7 @@ namespace Squidex.Pipeline
clientId = clientId.Split(':')[0];
var contributor = app.Clients.FirstOrDefault(x => string.Equals(x.ClientName, clientId, StringComparison.OrdinalIgnoreCase));
var contributor = app.Clients.FirstOrDefault(x => string.Equals(x.ClientId, clientId, StringComparison.OrdinalIgnoreCase));
return contributor != null ? PermissionLevel.Owner : PermissionLevel.Editor;
}

BIN
src/Squidex/app-libs/icomoon/fonts/icomoon.eot

Binary file not shown.

1
src/Squidex/app-libs/icomoon/fonts/icomoon.svg

@ -14,4 +14,5 @@
<glyph unicode="&#xe92e;" glyph-name="schemas" d="M1024 640l-512 256-512-256 512-256 512 256zM512 811.030l342.058-171.030-342.058-171.030-342.058 171.030 342.058 171.030zM921.444 499.278l102.556-51.278-512-256-512 256 102.556 51.278 409.444-204.722zM921.444 307.278l102.556-51.278-512-256-512 256 102.556 51.278 409.444-204.722z" />
<glyph unicode="&#xe994;" glyph-name="settings" d="M933.79 349.75c-53.726 93.054-21.416 212.304 72.152 266.488l-100.626 174.292c-28.75-16.854-62.176-26.518-97.846-26.518-107.536 0-194.708 87.746-194.708 195.99h-201.258c0.266-33.41-8.074-67.282-25.958-98.252-53.724-93.056-173.156-124.702-266.862-70.758l-100.624-174.292c28.97-16.472 54.050-40.588 71.886-71.478 53.638-92.908 21.512-211.92-71.708-266.224l100.626-174.292c28.65 16.696 61.916 26.254 97.4 26.254 107.196 0 194.144-87.192 194.7-194.958h201.254c-0.086 33.074 8.272 66.57 25.966 97.218 53.636 92.906 172.776 124.594 266.414 71.012l100.626 174.29c-28.78 16.466-53.692 40.498-71.434 71.228zM512 240.668c-114.508 0-207.336 92.824-207.336 207.334 0 114.508 92.826 207.334 207.336 207.334 114.508 0 207.332-92.826 207.332-207.334-0.002-114.51-92.824-207.334-207.332-207.334z" />
<glyph unicode="&#xe9a6;" glyph-name="dashboard" d="M512 896c282.77 0 512-229.23 512-512 0-192.792-106.576-360.666-264.008-448h-495.984c-157.432 87.334-264.008 255.208-264.008 448 0 282.77 229.23 512 512 512zM801.914 94.086c77.438 77.44 120.086 180.398 120.086 289.914h-90v64h85.038c-7.014 44.998-21.39 88.146-42.564 128h-106.474v64h64.284c-9.438 11.762-19.552 23.096-30.37 33.914-46.222 46.22-101.54 80.038-161.914 99.798v-69.712h-64v85.040c-20.982 3.268-42.36 4.96-64 4.96s-43.018-1.69-64-4.96v-85.040h-64v69.712c-60.372-19.76-115.692-53.576-161.914-99.798-10.818-10.818-20.932-22.152-30.37-33.914h64.284v-64h-106.476c-21.174-39.854-35.552-83.002-42.564-128h85.040v-64h-90c0-109.516 42.648-212.474 120.086-289.914 10.71-10.71 21.924-20.728 33.56-30.086h192.354l36.572 512h54.856l36.572-512h192.354c11.636 9.358 22.852 19.378 33.56 30.086z" />
<glyph unicode="&#xe9ac;" glyph-name="bin" d="M128 640v-640c0-35.2 28.8-64 64-64h576c35.2 0 64 28.8 64 64v640h-704zM320 64h-64v448h64v-448zM448 64h-64v448h64v-448zM576 64h-64v448h64v-448zM704 64h-64v448h64v-448zM848 832h-208v80c0 26.4-21.6 48-48 48h-224c-26.4 0-48-21.6-48-48v-80h-208c-26.4 0-48-21.6-48-48v-80h832v80c0 26.4-21.6 48-48 48zM576 832h-192v63.198h192v-63.198z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src/Squidex/app-libs/icomoon/fonts/icomoon.ttf

Binary file not shown.

BIN
src/Squidex/app-libs/icomoon/fonts/icomoon.woff

Binary file not shown.

33
src/Squidex/app-libs/icomoon/selection.json

@ -158,6 +158,39 @@
"setId": 2,
"iconIdx": 166
},
{
"icon": {
"paths": [
"M128 320v640c0 35.2 28.8 64 64 64h576c35.2 0 64-28.8 64-64v-640h-704zM320 896h-64v-448h64v448zM448 896h-64v-448h64v448zM576 896h-64v-448h64v448zM704 896h-64v-448h64v448z",
"M848 128h-208v-80c0-26.4-21.6-48-48-48h-224c-26.4 0-48 21.6-48 48v80h-208c-26.4 0-48 21.6-48 48v80h832v-80c0-26.4-21.6-48-48-48zM576 128h-192v-63.198h192v63.198z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"bin",
"trashcan",
"remove",
"delete",
"recycle",
"dispose"
],
"defaultCode": 59820,
"grid": 16
},
"attrs": [],
"properties": {
"ligatures": "bin, trashcan",
"name": "bin",
"id": 172,
"order": 9,
"prevSize": 32,
"code": 59820
},
"setIdx": 0,
"setId": 2,
"iconIdx": 172
},
{
"icon": {
"paths": [

13
src/Squidex/app-libs/icomoon/style.css

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?vsixlk');
src: url('fonts/icomoon.eot?vsixlk#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?vsixlk') format('truetype'),
url('fonts/icomoon.woff?vsixlk') format('woff'),
url('fonts/icomoon.svg?vsixlk#icomoon') format('svg');
src: url('fonts/icomoon.eot?k706v2');
src: url('fonts/icomoon.eot?k706v2#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?k706v2') format('truetype'),
url('fonts/icomoon.woff?k706v2') format('woff'),
url('fonts/icomoon.svg?k706v2#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
@ -39,6 +39,9 @@
.icon-dashboard:before {
content: "\e9a6";
}
.icon-bin:before {
content: "\e9ac";
}
.icon-plus:before {
content: "\e901";
}

104
src/Squidex/app/components/internal/app/settings/contributors-page.component.html

@ -9,58 +9,60 @@
</h1>
</div>
<div class="layout-middle-content">
<div class="card">
<div class="card-block">
<table class="table table-borderless-top table-fixed contributors-table">
<colgroup>
<col style="width: 100%" />
<col style="width: 150px" />
<col style="width: 110px" />
</colgroup>
<table class="table table-items table-fixed">
<colgroup>
<col style="width: 50px" />
<col style="width: 50%" />
<col style="width: 50%" />
<col style="width: 150px" />
<col style="width: 60px" />
</colgroup>
<template ngFor let-contributor [ngForOf]="appContributors">
<tr>
<td>
<img class="user-picture" [attr.src]="pictureUrl(contributor) | async" />
</td>
<td>
<span class="user-name">{{displayName(contributor) | async}}</span>
</td>
<td>
<span class="user-email">{{email(contributor) | async}}</span>
</td>
<td>
<select class="form-control" [(ngModel)]="contributor.permission" (ngModelChange)="saveContributor(contributor)" [disabled]="currrentUserId === contributor.contributorId">
<option *ngFor="let permission of usersPermissions">{{permission}}</option>
</select>
</td>
<td>
<button class="btn btn-link btn-danger" [disabled]="currrentUserId === contributor.contributorId" (click)="removeContributor(contributor)">
<i class="icon-bin"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</template>
</table>
<div class="table-items-footer">
<form class="form-inline" (submit)="assignContributor()" >
<div class="form-group">
<ng2-completer
[autoMatch]="true"
[dataService]="usersDataSource"
[minSearchLength]="3"
[placeholder]="'Search user by e-mail'"
[pause]="300"
[clearSelected]="false"
[textSearching]="'Searching...'"
[inputName]="contributor"
[ngModel]="selectedUserName"
[ngModelOptions]="{standalone: true}"
(selected)="selectUser($event)">
</ng2-completer>
</div>
<tr *ngFor="let contributor of appContributors">
<td class="col-xs-7">
<img class="user-picture" [attr.src]="pictureUrl(contributor) | async" />
<span class="user-name">
{{displayName(contributor) | async}}
</span>
<span class="user-email">
{{email(contributor) | async}}
</span>
</td>
<td>
<select class="form-control" [(ngModel)]="contributor.permission" (ngModelChange)="saveContributor(contributor)" [disabled]="currrentUserId === contributor.contributorId">
<option *ngFor="let permission of usersPermissions">{{permission}}</option>
</select>
</td>
<td>
<button class="btn btn-block btn-danger" [disabled]="currrentUserId === contributor.contributorId" (click)="removeContributor(contributor)">Remove</button>
</td>
</tr>
</table>
</div>
<div class="card-footer">
<form class="form-inline" (submit)="assignContributor()" >
<div class="form-group">
<ng2-completer
[autoMatch]="true"
[dataService]="usersDataSource"
[minSearchLength]="3"
[placeholder]="'Search user by e-mail'"
[pause]="300"
[clearSelected]="false"
[textSearching]="'Searching...'"
[inputName]="contributor"
[ngModel]="selectedUserName"
[ngModelOptions]="{standalone: true}"
(selected)="selectUser($event)">
</ng2-completer>
</div>
<button type="submit" class="btn btn-success" [disabled]="!selectedUser">Add Contributor</button>
</form>
</div>
<button type="submit" class="btn btn-success" [disabled]="!selectedUser">Add Contributor</button>
</form>
</div>
</div>
</div>

4
src/Squidex/app/components/internal/app/settings/contributors-page.component.scss

@ -18,17 +18,15 @@
.user {
&-picture {
@include circle(2.2rem);
float: left;
}
&-name,
&-email {
@include truncate;
padding-left: 10px;
}
&-email {
font-size: .8rem;
font-style: italic;
font-size: .8rem;
}
}

47
src/Squidex/app/theme/_bootstrap.scss

@ -13,8 +13,12 @@
}
&-borderless-top {
tr:first-child td {
border: 0;
tr {
&:first-child {
td {
border: 0;
}
}
}
}
@ -23,6 +27,39 @@
vertical-align: middle;
}
}
&-items {
& {
margin-bottom: .25rem;
}
td {
padding: 10px 12px;
margin: 0;
margin-bottom: 10px;
vertical-align: middle;
}
tr {
padding: 10px 12px;
background: $color-table;
border: 1px solid $color-border;
border-bottom: 2px solid $color-border;
margin-bottom: 10px;
}
&-footer {
@include border-radius(.25rem);
padding: 10px 12px;
background: $color-table;
border: 1px solid $color-border;
border-bottom: 2px solid $color-border;
}
.spacer {
height: .25rem;
}
}
}
.form-hint {
@ -115,4 +152,10 @@
.dropdown-menu {
@include box-shadow(0, 6px, 16px, .2px);
border: 0;
}
.btn-link {
&.btn-danger {
color: $color-theme-error;
}
}

2
src/Squidex/app/theme/_layout.scss

@ -53,7 +53,7 @@ h1 {
&-left {
border-right: 1px solid $color-border;
}
&-middle {
& {
@include flex-grow(1);

1
src/Squidex/app/theme/_vars.scss

@ -27,6 +27,7 @@ $color-theme-error: #f00;
$color-theme-error-dark: darken($color-theme-error, 5%);
$color-accent-dark: #fff;
$color-table: #fff;
$color-card-footer: #fff;

2
src/Squidex/appsettings.json

@ -10,7 +10,7 @@
"eventStore": {
"ipAddress": "127.0.0.1",
"port": 1113,
"prefix": "squidex_v2",
"prefix": "squidex_v3",
"username": "admin",
"password": "changeit"
}

21
tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -164,7 +164,7 @@ namespace Squidex.Write.Apps
var timestamp = DateTime.Today;
var command = new AttachClient { ClientName = clientName, AggregateId = Id, Timestamp = timestamp };
var command = new AttachClient { ClientId = clientName, AggregateId = Id, Timestamp = timestamp };
var context = new CommandContext(command);
await TestUpdate(app, async _ =>
@ -178,13 +178,28 @@ namespace Squidex.Write.Apps
new AppClient(clientName, clientSecret, timestamp.AddYears(1)));
}
[Fact]
public async Task RenameClient_should_update_domain_object()
{
CreateApp()
.AttachClient(new AttachClient { ClientId = clientName }, clientSecret);
var command = new RenameClient { AggregateId = Id, ClientId = clientName, Name = "New Name" };
var context = new CommandContext(command);
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task RevokeClient_should_update_domain_object()
{
CreateApp()
.AttachClient(new AttachClient { ClientName = clientName }, clientSecret);
.AttachClient(new AttachClient { ClientId = clientName }, clientSecret);
var command = new RevokeClient { AggregateId = Id, ClientName = clientName };
var command = new RevokeClient { AggregateId = Id, ClientId = clientName };
var context = new CommandContext(command);
await TestUpdate(app, async _ =>

68
tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs

@ -29,7 +29,8 @@ namespace Squidex.Write.Apps
private readonly UserToken user = new UserToken("subject", Guid.NewGuid().ToString());
private readonly string contributorId = Guid.NewGuid().ToString();
private readonly string clientSecret = Guid.NewGuid().ToString();
private readonly string clientName = "client";
private readonly string clientId = "client";
private readonly string clientNewName = "My Client";
private readonly List<Language> languages = new List<Language> { Language.GetLanguage("de") };
public AppDomainObjectTests()
@ -189,7 +190,7 @@ namespace Squidex.Write.Apps
[Fact]
public void AttachClient_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() => sut.AttachClient(new AttachClient { ClientName = clientName }, clientSecret));
Assert.Throws<DomainException>(() => sut.AttachClient(new AttachClient { ClientId = clientId }, clientSecret));
}
[Fact]
@ -198,7 +199,7 @@ namespace Squidex.Write.Apps
CreateApp();
Assert.Throws<ValidationException>(() => sut.AttachClient(new AttachClient(), clientSecret));
Assert.Throws<ValidationException>(() => sut.AttachClient(new AttachClient { ClientName = string.Empty }, clientSecret));
Assert.Throws<ValidationException>(() => sut.AttachClient(new AttachClient { ClientId = string.Empty }, clientSecret));
}
[Fact]
@ -206,9 +207,9 @@ namespace Squidex.Write.Apps
{
CreateApp();
sut.AttachClient(new AttachClient { ClientName = clientName }, clientSecret);
sut.AttachClient(new AttachClient { ClientId = clientId }, clientSecret);
Assert.Throws<ValidationException>(() => sut.AttachClient(new AttachClient { ClientName = clientName }, clientSecret));
Assert.Throws<ValidationException>(() => sut.AttachClient(new AttachClient { ClientId = clientId }, clientSecret));
}
[Fact]
@ -218,7 +219,7 @@ namespace Squidex.Write.Apps
CreateApp();
sut.AttachClient(new AttachClient { ClientName = clientName, Timestamp = now }, clientSecret);
sut.AttachClient(new AttachClient { ClientId = clientId, Timestamp = now }, clientSecret);
Assert.False(sut.Contributors.ContainsKey(contributorId));
@ -226,14 +227,14 @@ namespace Squidex.Write.Apps
.ShouldBeEquivalentTo(
new IEvent[]
{
new AppClientAttached { ClientName = clientName, ClientSecret = clientSecret, ExpiresUtc = now.AddYears(1) }
new AppClientAttached { ClientId = clientId, ClientSecret = clientSecret, ExpiresUtc = now.AddYears(1) }
});
}
[Fact]
public void RevokeKey_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() => sut.RevokeClient(new RevokeClient { ClientName = "not-found" }));
Assert.Throws<DomainException>(() => sut.RevokeClient(new RevokeClient { ClientId = "not-found" }));
}
[Fact]
@ -242,7 +243,7 @@ namespace Squidex.Write.Apps
CreateApp();
Assert.Throws<ValidationException>(() => sut.RevokeClient(new RevokeClient()));
Assert.Throws<ValidationException>(() => sut.RevokeClient(new RevokeClient { ClientName = string.Empty }));
Assert.Throws<ValidationException>(() => sut.RevokeClient(new RevokeClient { ClientId = string.Empty }));
}
[Fact]
@ -250,7 +251,7 @@ namespace Squidex.Write.Apps
{
CreateApp();
Assert.Throws<ValidationException>(() => sut.RevokeClient(new RevokeClient { ClientName = "not-found" }));
Assert.Throws<ValidationException>(() => sut.RevokeClient(new RevokeClient { ClientId = "not-found" }));
}
[Fact]
@ -258,14 +259,55 @@ namespace Squidex.Write.Apps
{
CreateApp();
sut.AttachClient(new AttachClient { ClientName = clientName }, clientSecret);
sut.RevokeClient(new RevokeClient { ClientName = clientName });
sut.AttachClient(new AttachClient { ClientId = clientId }, clientSecret);
sut.RevokeClient(new RevokeClient { ClientId = clientId });
sut.GetUncomittedEvents().Select(x => x.Payload).Skip(1).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
new AppClientRevoked { ClientName = clientSecret }
new AppClientRevoked { ClientId = clientSecret }
});
}
[Fact]
public void RenameKey_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() => sut.RenameClient(new RenameClient { ClientId = "not-found", Name = clientNewName }));
}
[Fact]
public void RenameClient_should_throw_if_command_is_not_valid()
{
CreateApp();
Assert.Throws<ValidationException>(() => sut.RenameClient(new RenameClient()));
Assert.Throws<ValidationException>(() => sut.RenameClient(new RenameClient { ClientId = string.Empty }));
}
[Fact]
public void RenameClient_should_throw_if_client_not_found()
{
CreateApp();
Assert.Throws<ValidationException>(() => sut.RenameClient(new RenameClient { ClientId = "not-found", Name = clientNewName }));
}
[Fact]
public void RenameClient_should_create_events()
{
CreateApp();
sut.AttachClient(new AttachClient { ClientId = clientId }, clientSecret);
sut.RenameClient(new RenameClient { ClientId = clientId, Name = clientNewName });
Assert.Equal(clientNewName, sut.Clients[clientId].Name);
sut.GetUncomittedEvents().Select(x => x.Payload).Skip(1).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
new AppClientRenamed { ClientId = clientId, Name = clientNewName }
});
}

Loading…
Cancel
Save