Browse Source

Merge pull request #264 from Squidex/feature-privacy-improvement

Improved privacy by hiding the email of the user.
pull/272/head
Sebastian Stehle 8 years ago
committed by GitHub
parent
commit
12bd00071f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .drone.yml
  2. 2
      LICENSE.txt
  3. 4
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  4. 23
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  5. 12
      src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs
  6. 10
      src/Squidex.Domain.Users/UserExtensions.cs
  7. 3
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  8. 2
      src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  9. 2
      src/Squidex.Shared/Users/IUserResolver.cs
  10. 11
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  11. 1
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  12. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs
  13. 20
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs
  14. 2
      src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs
  15. 9
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  16. 1
      src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  17. 9
      src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs
  18. 26
      src/Squidex/Areas/Api/Controllers/Users/Models/PublicUserDto.cs
  19. 9
      src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs
  20. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs
  21. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs
  22. 4
      src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
  23. 8
      src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  24. 2
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs
  25. 3
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  26. 2
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs
  27. 12
      src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
  28. 1
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  29. 14
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  30. 11
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss
  31. 13
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  32. 7
      src/Squidex/app/framework/angular/autocomplete.component.ts
  33. 40
      src/Squidex/app/shared/components/pipes.ts
  34. 6
      src/Squidex/app/shared/module.ts
  35. 13
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  36. 16
      src/Squidex/app/shared/services/app-contributors.service.ts
  37. 20
      src/Squidex/app/shared/services/users-provider.service.spec.ts
  38. 10
      src/Squidex/app/shared/services/users-provider.service.ts
  39. 68
      src/Squidex/app/shared/services/users.service.spec.ts
  40. 37
      src/Squidex/app/shared/services/users.service.ts
  41. 12
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs
  42. 34
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs
  43. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs

5
.drone.yml

@ -1,3 +1,8 @@
clone:
git:
image: plugins/git:next
pull: true
pipeline:
test_pull_request:
image: docker

2
LICENSE.txt

@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

4
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -70,11 +70,13 @@ namespace Squidex.Domain.Apps.Entities.Apps
});
case AssignContributor assigneContributor:
return UpdateAsync(assigneContributor, async c =>
return UpdateReturnAsync(assigneContributor, async c =>
{
await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId));
AssignContributor(c);
return EntityCreatedResult.Create(c.ContributorId, NewVersion);
});
case RemoveContributor removeContributor:

23
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -34,20 +34,27 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
}
else
{
if (await users.FindByIdAsync(command.ContributorId) == null)
var user = await users.FindByIdOrEmailAsync(command.ContributorId);
if (user == null)
{
error(new ValidationError("Cannot find contributor id.", nameof(command.ContributorId)));
}
else if (contributors.TryGetValue(command.ContributorId, out var existing))
else
{
if (existing == command.Permission)
command.ContributorId = user.Id;
if (contributors.TryGetValue(command.ContributorId, out var existing))
{
error(new ValidationError("Contributor has already this permission.", nameof(command.Permission)));
if (existing == command.Permission)
{
error(new ValidationError("Contributor has already this permission.", nameof(command.Permission)));
}
}
else if (plan.MaxContributors == contributors.Count)
{
error(new ValidationError("You have reached the maximum number of contributors for your plan."));
}
}
else if (plan.MaxContributors == contributors.Count)
{
error(new ValidationError("You have reached the maximum number of contributors for your plan."));
}
}
});

12
src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs

@ -12,6 +12,7 @@ using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Tasks;
@ -72,9 +73,16 @@ namespace Squidex.Domain.Users.MongoDb
return new MongoUser { Email = email, UserName = email };
}
public async Task<IUser> FindByIdAsync(string id)
public async Task<IUser> FindByIdOrEmailAsync(string id)
{
return await Collection.Find(x => x.Id == id).FirstOrDefaultAsync();
if (ObjectId.TryParse(id, out var parsed))
{
return await Collection.Find(x => x.Id == id).FirstOrDefaultAsync();
}
else
{
return await Collection.Find(x => x.NormalizedEmail == id.ToUpperInvariant()).FirstOrDefaultAsync();
}
}
public async Task<IUser> FindByIdAsync(string userId, CancellationToken cancellationToken)

10
src/Squidex.Domain.Users/UserExtensions.cs

@ -35,6 +35,11 @@ namespace Squidex.Domain.Users
user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, GravatarHelper.CreatePictureUrl(email));
}
public static void SetHidden(this IUser user, bool value)
{
user.SetClaim(SquidexClaimTypes.SquidexHidden, value.ToString());
}
public static void SetConsent(this IUser user)
{
user.SetClaim(SquidexClaimTypes.SquidexConsent, "true");
@ -45,6 +50,11 @@ namespace Squidex.Domain.Users
user.SetClaim(SquidexClaimTypes.SquidexConsentForEmails, value.ToString());
}
public static bool IsHidden(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexHidden, "true");
}
public static bool HasConsent(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexConsent, "true");

3
src/Squidex.Domain.Users/UserManagerExtensions.cs

@ -71,8 +71,9 @@ namespace Squidex.Domain.Users
return user;
}
public static Task<IdentityResult> UpdateAsync(this UserManager<IUser> userManager, IUser user, string email, string displayName)
public static Task<IdentityResult> UpdateAsync(this UserManager<IUser> userManager, IUser user, string email, string displayName, bool hidden)
{
user.SetHidden(hidden);
user.SetEmail(email);
user.SetDisplayName(displayName);

2
src/Squidex.Shared/Identity/SquidexClaimTypes.cs

@ -17,6 +17,8 @@ namespace Squidex.Shared.Identity
public static readonly string SquidexConsentForEmails = "urn:squidex:consent:emails";
public static readonly string SquidexHidden = "urn:squidex:hidden";
public static readonly string Prefix = "urn:squidex:";
}
}

2
src/Squidex.Shared/Users/IUserResolver.cs

@ -11,6 +11,6 @@ namespace Squidex.Shared.Users
{
public interface IUserResolver
{
Task<IUser> FindByIdAsync(string id);
Task<IUser> FindByIdOrEmailAsync(string idOrEmail);
}
}

11
src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -65,19 +65,24 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="app">The name of the app.</param>
/// <param name="request">Contributor object that needs to be added to the app.</param>
/// <returns>
/// 204 => User assigned to app.
/// 200 => User assigned to app.
/// 400 => User is already assigned to the app or not found.
/// 404 => App not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorAssignedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(1)]
public async Task<IActionResult> PostContributor(string app, [FromBody] AssignAppContributorDto request)
{
await CommandBus.PublishAsync(SimpleMapper.Map(request, new AssignContributor()));
var command = SimpleMapper.Map(request, new AssignContributor());
var context = await CommandBus.PublishAsync(command);
return NoContent();
var result = context.Result<EntityCreatedResult<string>>();
var response = new ContributorAssignedDto { ContributorId = result.IdOrValue };
return Ok(response);
}
/// <summary>

1
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -99,7 +99,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
public async Task<IActionResult> PostApp([FromBody] CreateAppDto request)
{
var command = SimpleMapper.Map(request, new CreateApp());
var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>();

2
src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs

@ -15,7 +15,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public sealed class AssignAppContributorDto
{
/// <summary>
/// The id of the user to add to the app.
/// The id or email of the user to add to the app.
/// </summary>
[Required]
public string ContributorId { get; set; }

20
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class ContributorAssignedDto
{
/// <summary>
/// The id of the user that has been assigned as contributor.
/// </summary>
[Required]
public string ContributorId { get; set; }
}
}

2
src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs

@ -268,7 +268,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
await contentQuery.FindSchemaAsync(App, name);
var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() };
var context = await CommandBus.PublishAsync(command);
var result = context.Result<ContentDataChangedResult>();
@ -301,7 +300,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
await contentQuery.FindSchemaAsync(App, name);
var command = new PatchContent { ContentId = id, Data = request.ToCleaned() };
var context = await CommandBus.PublishAsync(command);
var result = context.Result<ContentDataChangedResult>();

9
src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -11,6 +11,7 @@ using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Schemas
@ -50,13 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ApiCosts(1)]
public async Task<IActionResult> PostField(string app, string name, [FromBody] AddFieldDto request)
{
var command = new AddField
{
Name = request.Name,
Partitioning = request.Partitioning,
Properties = request.Properties.ToProperties()
};
var command = SimpleMapper.Map(request, new AddField { Properties = request.Properties.ToProperties() });
var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<long>>();

1
src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -120,7 +120,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
public async Task<IActionResult> PostSchema(string app, [FromBody] CreateSchemaDto request)
{
var command = request.ToCommand();
var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>();

9
src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs

@ -11,13 +11,22 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
{
public sealed class CreateUserDto
{
/// <summary>
/// The email of the user. Unique value.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; }
/// <summary>
/// The display name (usually first name and last name) of the user.
/// </summary>
[Required]
public string DisplayName { get; set; }
/// <summary>
/// The password of the user.
/// </summary>
[Required]
public string Password { get; set; }
}

26
src/Squidex/Areas/Api/Controllers/Users/Models/PublicUserDto.cs

@ -0,0 +1,26 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.Users.Models
{
public sealed class PublicUserDto
{
/// <summary>
/// The id of the user.
/// </summary>
[Required]
public string Id { get; set; }
/// <summary>
/// The display name (usually first name and last name) of the user.
/// </summary>
[Required]
public string DisplayName { get; set; }
}
}

9
src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs

@ -11,13 +11,22 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
{
public sealed class UpdateUserDto
{
/// <summary>
/// The email of the user. Unique value.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; }
/// <summary>
/// The display name (usually first name and last name) of the user.
/// </summary>
[Required]
public string DisplayName { get; set; }
/// <summary>
/// The password of the user.
/// </summary>
public string Password { get; set; }
}
}

6
src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs

@ -11,10 +11,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
{
public sealed class UserCreatedDto
{
/// <summary>
/// The id of the user.
/// </summary>
[Required]
public string Id { get; set; }
[Required]
public string PictureUrl { get; set; }
}
}

6
src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs

@ -23,12 +23,6 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
[Required]
public string Email { get; set; }
/// <summary>
/// The url to the profile picture of the user.
/// </summary>
[Required]
public string PictureUrl { get; set; }
/// <summary>
/// The display name (usually first name and last name) of the user.
/// </summary>

4
src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs

@ -82,7 +82,7 @@ namespace Squidex.Areas.Api.Controllers.Users
{
var user = await userManager.CreateAsync(userFactory, request.Email, request.DisplayName, request.Password);
var response = new UserCreatedDto { Id = user.Id, PictureUrl = user.PictureUrl() };
var response = new UserCreatedDto { Id = user.Id };
return Ok(response);
}
@ -129,7 +129,7 @@ namespace Squidex.Areas.Api.Controllers.Users
private static UserDto Map(IUser user)
{
return SimpleMapper.Map(user, new UserDto { DisplayName = user.DisplayName(), PictureUrl = user.PictureUrl() });
return SimpleMapper.Map(user, new UserDto { DisplayName = user.DisplayName() });
}
private bool IsSelf(string id)

8
src/Squidex/Areas/Api/Controllers/Users/UsersController.cs

@ -65,12 +65,12 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiAuthorize]
[HttpGet]
[Route("users/")]
[ProducesResponseType(typeof(UserDto[]), 200)]
[ProducesResponseType(typeof(PublicUserDto[]), 200)]
public async Task<IActionResult> GetUsers(string query)
{
var entities = await userManager.QueryByEmailAsync(query ?? string.Empty);
var models = entities.Select(x => SimpleMapper.Map(x, new UserDto { DisplayName = x.DisplayName(), PictureUrl = x.PictureUrl() })).ToArray();
var models = entities.Where(x => !x.IsHidden()).Select(x => SimpleMapper.Map(x, new UserDto { DisplayName = x.DisplayName() })).ToArray();
return Ok(models);
}
@ -86,7 +86,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiAuthorize]
[HttpGet]
[Route("users/{id}/")]
[ProducesResponseType(typeof(UserDto), 200)]
[ProducesResponseType(typeof(PublicUserDto), 200)]
public async Task<IActionResult> GetUser(string id)
{
var entity = await userManager.FindByIdAsync(id);
@ -96,7 +96,7 @@ namespace Squidex.Areas.Api.Controllers.Users
return NotFound();
}
var response = SimpleMapper.Map(entity, new UserDto { DisplayName = entity.DisplayName(), PictureUrl = entity.PictureUrl() });
var response = SimpleMapper.Map(entity, new UserDto { DisplayName = entity.DisplayName() });
return Ok(response);
}

2
src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs

@ -17,5 +17,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Required(ErrorMessage = "DisplayName is required.")]
public string DisplayName { get; set; }
public bool IsHidden { get; set; }
}
}

3
src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs

@ -83,7 +83,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/update/")]
public Task<IActionResult> UpdateProfile(ChangeProfileModel model)
{
return MakeChangeAsync(user => userManager.UpdateAsync(user, model.Email, model.DisplayName),
return MakeChangeAsync(user => userManager.UpdateAsync(user, model.Email, model.DisplayName, model.IsHidden),
"Account updated successfully.");
}
@ -195,6 +195,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
ExternalLogins = user.Logins,
ExternalProviders = externalProviders,
DisplayName = user.DisplayName(),
IsHidden = user.IsHidden(),
HasPassword = await userManager.HasPasswordAsync(user),
HasPasswordAuth = identityOptions.Value.AllowPasswordAuth,
SuccessMessage = successMessage

2
src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs

@ -22,6 +22,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
public string SuccessMessage { get; set; }
public bool IsHidden { get; set; }
public bool HasPassword { get; set; }
public bool HasPasswordAuth { get; set; }

12
src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml

@ -52,7 +52,7 @@
</div>
}
<input type="email" ap class="form-control" asp-for="Email" name="email" id="email" />
<input type="email" ap class="form-control" asp-for="Email" id="email" />
</div>
<div class="form-group">
@ -65,7 +65,15 @@
</div>
}
<input type="text" class="form-control" asp-for="DisplayName" name="displayName" id="displayName"/>
<input type="text" class="form-control" asp-for="DisplayName" id="displayName"/>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="IsHidden" id="hidden" />
<label class="form-check-label" for="hidden">Do not show my profile to other users</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>

1
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -68,7 +68,6 @@ export class UserPageComponent implements OnInit {
created.id,
requestDto.email,
requestDto.displayName,
created.pictureUrl!,
false);
this.ctx.notifyInfo('User created successfully.');

14
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -27,16 +27,13 @@
<td class="cell-auto">
<span class="user-name table-cell">{{contributor.contributorId | sqxUserName}}</span>
</td>
<td class="cell-auto">
<span class="user-email table-cell">{{contributor.contributorId | sqxUserEmail}}</span>
</td>
<td class="cell-time">
<select class="form-control" [ngModel]="contributor.permission" (ngModelChange)="changePermission(contributor, $event)" [disabled]="userId === contributor.contributorId">
<option *ngFor="let permission of usersPermissions" [ngValue]="permission">{{permission}}</option>
</select>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-link btn-danger" [disabled]="userId === contributor.contributorId" (click)="removeContributor(contributor)">
<button *ngIf="ctx.userId !== contributor.contributorId" type="button" class="btn btn-link btn-danger" (click)="removeContributor(contributor)">
<i class="icon-bin2"></i>
</button>
</td>
@ -50,12 +47,13 @@
<form [formGroup]="addContributorForm" (ngSubmit)="assignContributor()">
<div class="row no-gutters">
<div class="col">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user" displayProperty="email">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user" displayProperty="displayName">
<ng-template let-user="$implicit">
<img class="user-picture autocomplete-user-picture" [attr.src]="user | sqxUserDtoPicture" />
<span class="autocomplete-user">
<img class="user-picture autocomplete-user-picture" [attr.src]="user | sqxUserDtoPicture" />
<span class="user-name autocomplete-user-name">{{user.displayName}}</span>
<span class="user-email autocomplete-user-email">{{user.email}}</span>
<span class="user-name autocomplete-user-name">{{user.displayName}}</span>
</span>
</ng-template>
</sqx-autocomplete>
</div>

11
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss

@ -2,14 +2,11 @@
@import '_mixins';
.autocomplete-user {
&-picture {
float: left;
margin-top: .4rem;
& {
@include truncate;
}
&-name,
&-email {
@include truncate;
margin-left: 3rem;
&-name {
margin-left: .25rem;
}
}

13
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -16,6 +16,7 @@ import {
AppContributorsService,
AutocompleteSource,
HistoryChannelUpdated,
PublicUserDto,
UsersService
} from 'shared';
@ -110,16 +111,20 @@ export class ContributorsPageComponent implements OnInit {
}
public assignContributor() {
const requestDto = new AppContributorDto(this.addContributorForm.controls['user'].value.id, 'Editor');
let value: any = this.addContributorForm.controls['user'].value;
if (value instanceof PublicUserDto) {
value = value.id;
}
const requestDto = new AppContributorDto(value, 'Editor');
this.appContributorsService.postContributor(this.ctx.appName, requestDto, this.appContributors.version)
.subscribe(dto => {
this.updateContributors(this.appContributors.addContributor(requestDto, dto.version));
this.updateContributors(this.appContributors.addContributor(new AppContributorDto(dto.payload.contributorId, requestDto.permission), dto.version));
this.resetContributorForm();
}, error => {
this.ctx.notifyError(error);
this.resetContributorForm();
});
}

7
src/Squidex/app/framework/angular/autocomplete.component.ts

@ -61,15 +61,18 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
public ngOnInit() {
this.subscription =
this.queryInput.valueChanges
.do(query => {
this.callChange(query);
})
.map(query => <string>query)
.map(query => query ? query.trim() : query)
.distinctUntilChanged()
.debounceTime(200)
.do(query => {
if (!query) {
this.reset();
}
})
.distinctUntilChanged()
.debounceTime(200)
.filter(query => !!query && !!this.source)
.switchMap(query => this.source.find(query)).catch(error => Observable.of([]))
.subscribe(items => {

40
src/Squidex/app/shared/components/pipes.ts

@ -10,7 +10,7 @@ import { Observable, Subscription } from 'rxjs';
import { ApiUrlConfig, MathHelper } from 'framework';
import { UserDto, UsersProviderService } from './../declarations-base';
import { PublicUserDto, UsersProviderService } from './../declarations-base';
class UserAsyncPipe implements OnDestroy {
private lastUserId: string;
@ -88,42 +88,6 @@ export class UserNameRefPipe extends UserAsyncPipe implements PipeTransform {
}
}
@Pipe({
name: 'sqxUserEmail',
pure: false
})
export class UserEmailPipe extends UserAsyncPipe implements PipeTransform {
constructor(users: UsersProviderService, changeDetector: ChangeDetectorRef) {
super(users, changeDetector);
}
public transform(userId: string): string | null {
return super.transformInternal(userId, users => users.getUser(userId).map(u => u.email));
}
}
@Pipe({
name: 'sqxUserEmailRef',
pure: false
})
export class UserEmailRefPipe extends UserAsyncPipe implements PipeTransform {
constructor(users: UsersProviderService, changeDetector: ChangeDetectorRef) {
super(users, changeDetector);
}
public transform(userId: string): string | null {
return super.transformInternal(userId, users => {
const parts = userId.split(':');
if (parts[0] === 'subject') {
return users.getUser(parts[1]).map(u => u.email);
} else {
return Observable.of(null);
}
});
}
}
@Pipe({
name: 'sqxUserDtoPicture',
pure: false
@ -134,7 +98,7 @@ export class UserDtoPicture implements PipeTransform {
) {
}
public transform(user: UserDto): string | null {
public transform(user: PublicUserDto): string | null {
return this.apiUrl.buildUrl(`api/users/${user.id}/picture`);
}
}

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

@ -53,8 +53,6 @@ import {
UnsetAppGuard,
UsagesService,
UserDtoPicture,
UserEmailPipe,
UserEmailRefPipe,
UserNamePipe,
UserNameRefPipe,
UserIdPicturePipe,
@ -83,8 +81,6 @@ import {
LanguageSelectorComponent,
MarkdownEditorComponent,
UserDtoPicture,
UserEmailPipe,
UserEmailRefPipe,
UserIdPicturePipe,
UserNamePipe,
UserNameRefPipe,
@ -104,8 +100,6 @@ import {
LanguageSelectorComponent,
MarkdownEditorComponent,
UserDtoPicture,
UserEmailPipe,
UserEmailRefPipe,
UserIdPicturePipe,
UserNamePipe,
UserNameRefPipe,

13
src/Squidex/app/shared/services/app-contributors.service.spec.ts

@ -14,7 +14,8 @@ import {
AppContributorDto,
AppContributorsDto,
AppContributorsService,
Version
Version,
ContributorAssignedDto
} from './../';
describe('AppContributorsDto', () => {
@ -122,14 +123,20 @@ describe('AppContributorsService', () => {
const dto = new AppContributorDto('123', 'Owner');
appContributorsService.postContributor('my-app', dto, version).subscribe();
let contributorAssignedDto: ContributorAssignedDto | null = null;
appContributorsService.postContributor('my-app', dto, version).subscribe(result => {
contributorAssignedDto = result.payload;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/contributors');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({});
req.flush({ contributorId: '123' });
expect(contributorAssignedDto!.contributorId).toEqual('123');
}));
it('should make delete request to remove contributor',

16
src/Squidex/app/shared/services/app-contributors.service.ts

@ -58,6 +58,13 @@ export class AppContributorDto {
}
}
export class ContributorAssignedDto {
constructor(
public readonly contributorId: string
) {
}
}
@Injectable()
export class AppContributorsService {
constructor(
@ -87,10 +94,17 @@ export class AppContributorsService {
.pretifyError('Failed to load contributors. Please reload.');
}
public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable<Versioned<any>> {
public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable<Versioned<ContributorAssignedDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return HTTP.postVersioned(this.http, url, dto, version)
.map(response => {
const body: any = response.payload.body;
const result = new ContributorAssignedDto(body.contributorId);
return new Versioned(response.version, result);
})
.do(() => {
this.analytics.trackEvent('Contributor', 'Configured', appName);
})

20
src/Squidex/app/shared/services/users-provider.service.spec.ts

@ -11,7 +11,7 @@ import { IMock, Mock, Times } from 'typemoq';
import {
AuthService,
Profile,
UserDto,
PublicUserDto,
UsersProviderService,
UsersService
} from './../';
@ -28,12 +28,12 @@ describe('UsersProviderService', () => {
});
it('should return users service when user not cached', () => {
const user = new UserDto('123', 'mail@domain.com', 'User1', 'path/to/image', true);
const user = new PublicUserDto('123', 'User1');
usersService.setup(x => x.getUser('123'))
.returns(() => Observable.of(user)).verifiable(Times.once());
let resultingUser: UserDto | null = null;
let resultingUser: PublicUserDto | null = null;
usersProviderService.getUser('123').subscribe(result => {
resultingUser = result;
@ -45,14 +45,14 @@ describe('UsersProviderService', () => {
});
it('should return provide user from cache', () => {
const user = new UserDto('123', 'mail@domain.com', 'User1', 'path/to/image', true);
const user = new PublicUserDto('123', 'User1');
usersService.setup(x => x.getUser('123'))
.returns(() => Observable.of(user)).verifiable(Times.once());
usersProviderService.getUser('123');
let resultingUser: UserDto | null = null;
let resultingUser: PublicUserDto | null = null;
usersProviderService.getUser('123').subscribe(result => {
resultingUser = result;
@ -64,7 +64,7 @@ describe('UsersProviderService', () => {
});
it('should return me when user is current user', () => {
const user = new UserDto('123', 'mail@domain.com', 'User1', 'path/to/image', true);
const user = new PublicUserDto('123', 'User1');
authService.setup(x => x.user)
.returns(() => new Profile(<any>{ profile: { sub: '123'}}));
@ -72,13 +72,13 @@ describe('UsersProviderService', () => {
usersService.setup(x => x.getUser('123'))
.returns(() => Observable.of(user)).verifiable(Times.once());
let resultingUser: UserDto | null = null;
let resultingUser: PublicUserDto | null = null;
usersProviderService.getUser('123').subscribe(result => {
resultingUser = result;
}).unsubscribe();
expect(resultingUser).toEqual(new UserDto('123', 'mail@domain.com', 'Me', 'path/to/image', true));
expect(resultingUser).toEqual(new PublicUserDto('123', 'Me'));
usersService.verifyAll();
});
@ -90,13 +90,13 @@ describe('UsersProviderService', () => {
usersService.setup(x => x.getUser('123'))
.returns(() => Observable.throw('NOT FOUND')).verifiable(Times.once());
let resultingUser: UserDto | null = null;
let resultingUser: PublicUserDto | null = null;
usersProviderService.getUser('123').subscribe(result => {
resultingUser = result;
}).unsubscribe();
expect(resultingUser).toEqual(new UserDto('NOT FOUND', 'NOT FOUND', 'NOT FOUND', null, false));
expect(resultingUser).toEqual(new PublicUserDto('Unknown', 'Unknown'));
usersService.verifyAll();
});

10
src/Squidex/app/shared/services/users-provider.service.ts

@ -8,13 +8,13 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { UserDto, UsersService } from './users.service';
import { PublicUserDto, UsersService } from './users.service';
import { AuthService } from './auth.service';
@Injectable()
export class UsersProviderService {
private readonly caches: { [id: string]: Observable<UserDto> } = {};
private readonly caches: { [id: string]: Observable<PublicUserDto> } = {};
constructor(
private readonly usersService: UsersService,
@ -22,14 +22,14 @@ export class UsersProviderService {
) {
}
public getUser(id: string, me: string | null = 'Me'): Observable<UserDto> {
public getUser(id: string, me: string | null = 'Me'): Observable<PublicUserDto> {
let result = this.caches[id];
if (!result) {
const request =
this.usersService.getUser(id)
.catch(error => {
return Observable.of(new UserDto('NOT FOUND', 'NOT FOUND', 'NOT FOUND', null, false));
return Observable.of(new PublicUserDto('Unknown', 'Unknown'));
})
.publishLast();
@ -41,7 +41,7 @@ export class UsersProviderService {
return result
.map(dto => {
if (me && this.authService.user && dto.id === this.authService.user.id) {
dto = new UserDto(dto.id, dto.email, me, dto.pictureUrl, dto.isLocked);
dto = new PublicUserDto(dto.id, me);
}
return dto;
}).share();

68
src/Squidex/app/shared/services/users.service.spec.ts

@ -11,6 +11,7 @@ import { inject, TestBed } from '@angular/core/testing';
import {
ApiUrlConfig,
CreateUserDto,
PublicUserDto,
UpdateUserDto,
UserDto,
UserManagementService,
@ -20,7 +21,7 @@ import {
describe('UserDto', () => {
it('should update email and display name property when unlocking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', true);
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', true);
const user_2 = user_1.update('qaisar@squidex.io', 'Qaisar');
expect(user_2.email).toEqual('qaisar@squidex.io');
@ -28,14 +29,14 @@ describe('UserDto', () => {
});
it('should update isLocked property when locking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', false);
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', false);
const user_2 = user_1.lock();
expect(user_2.isLocked).toBeTruthy();
});
it('should update isLocked property when unlocking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', true);
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', true);
const user_2 = user_1.unlock();
expect(user_2.isLocked).toBeFalsy();
@ -62,7 +63,7 @@ describe('UsersService', () => {
it('should make get request to get many users',
inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => {
let users: UserDto[] | null = null;
let users: PublicUserDto[] | null = null;
usersService.getUsers().subscribe(result => {
users = result;
@ -76,31 +77,25 @@ describe('UsersService', () => {
req.flush([
{
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
displayName: 'User1'
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
pictureUrl: 'path/to/image2',
isLocked: true
displayName: 'User2'
}
]);
expect(users).toEqual(
[
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true)
new PublicUserDto('123', 'User1'),
new PublicUserDto('456', 'User2')
]);
}));
it('should make get request with query to get many users',
inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => {
let users: UserDto[] | null = null;
let users: PublicUserDto[] | null = null;
usersService.getUsers('my-query').subscribe(result => {
users = result;
@ -114,31 +109,25 @@ describe('UsersService', () => {
req.flush([
{
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
displayName: 'User1'
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
pictureUrl: 'path/to/image2',
isLocked: true
displayName: 'User2'
}
]);
expect(users).toEqual(
[
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true)
new PublicUserDto('123', 'User1'),
new PublicUserDto('456', 'User2')
]);
}));
it('should make get request to get single user',
inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => {
let user: UserDto | null = null;
let user: PublicUserDto | null = null;
usersService.getUser('123').subscribe(result => {
user = result;
@ -149,15 +138,9 @@ describe('UsersService', () => {
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
});
req.flush({ id: '123', displayName: 'User1' });
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true));
expect(user).toEqual(new PublicUserDto('123', 'User1'));
}));
});
@ -199,14 +182,12 @@ describe('UserManagementService', () => {
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
pictureUrl: 'path/to/image2',
isLocked: true
}
]
@ -214,8 +195,8 @@ describe('UserManagementService', () => {
expect(users).toEqual(
new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true)
new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', true)
]));
}));
@ -255,8 +236,8 @@ describe('UserManagementService', () => {
expect(users).toEqual(
new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true)
new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', true)
]));
}));
@ -278,11 +259,10 @@ describe('UserManagementService', () => {
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
});
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true));
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', true));
}));
it('should make post request to create user',
@ -301,9 +281,9 @@ describe('UserManagementService', () => {
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ id: '123', pictureUrl: 'path/to/image1' });
req.flush({ id: '123' });
expect(user).toEqual(new UserDto('123', dto.email, dto.displayName, 'path/to/image1', false));
expect(user).toEqual(new UserDto('123', dto.email, dto.displayName, false));
}));
it('should make put request to update user',

37
src/Squidex/app/shared/services/users.service.ts

@ -26,21 +26,20 @@ export class UserDto {
public readonly id: string,
public readonly email: string,
public readonly displayName: string,
public readonly pictureUrl: string | null,
public readonly isLocked: boolean
) {
}
public update(email: string, displayName: string): UserDto {
return new UserDto(this.id, email, displayName, this.pictureUrl, this.isLocked);
return new UserDto(this.id, email, displayName, this.isLocked);
}
public lock(): UserDto {
return new UserDto(this.id, this.email, this.displayName, this.pictureUrl, true);
return new UserDto(this.id, this.email, this.displayName, true);
}
public unlock(): UserDto {
return new UserDto(this.id, this.email, this.displayName, this.pictureUrl, false);
return new UserDto(this.id, this.email, this.displayName, false);
}
}
@ -62,6 +61,14 @@ export class UpdateUserDto {
}
}
export class PublicUserDto {
constructor(
public readonly id: string,
public readonly displayName: string
) {
}
}
@Injectable()
export class UsersService {
constructor(
@ -70,7 +77,7 @@ export class UsersService {
) {
}
public getUsers(query?: string): Observable<UserDto[]> {
public getUsers(query?: string): Observable<PublicUserDto[]> {
const url = this.apiUrl.buildUrl(`api/users?query=${query || ''}`);
return HTTP.getVersioned<any>(this.http, url)
@ -80,30 +87,24 @@ export class UsersService {
const items: any[] = body;
return items.map(item => {
return new UserDto(
return new PublicUserDto(
item.id,
item.email,
item.displayName,
item.pictureUrl,
item.isLocked);
item.displayName);
});
})
.pretifyError('Failed to load users. Please reload.');
}
public getUser(id: string): Observable<UserDto> {
public getUser(id: string): Observable<PublicUserDto> {
const url = this.apiUrl.buildUrl(`api/users/${id}`);
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
return new UserDto(
return new PublicUserDto(
body.id,
body.email,
body.displayName,
body.pictureUrl,
body.isLocked);
body.displayName);
})
.pretifyError('Failed to load user. Please reload.');
}
@ -131,7 +132,6 @@ export class UserManagementService {
item.id,
item.email,
item.displayName,
item.pictureUrl,
item.isLocked);
});
@ -151,7 +151,6 @@ export class UserManagementService {
body.id,
body.email,
body.displayName,
body.pictureUrl,
body.isLocked);
})
.pretifyError('Failed to load user. Please reload.');
@ -164,7 +163,7 @@ export class UserManagementService {
.map(response => {
const body = response.payload.body;
return new UserDto(body.id, dto.email, dto.displayName, body.pictureUrl, false);
return new UserDto(body.id, dto.email, dto.displayName, false);
})
.pretifyError('Failed to create user. Please reload.');
}

12
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs

@ -27,6 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAppPlansProvider appPlansProvider = A.Fake<IAppPlansProvider>();
private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake<IAppPlanBillingManager>();
private readonly IUser user = A.Fake<IUser>();
private readonly IUserResolver userResolver = A.Fake<IUserResolver>();
private readonly string contributorId = Guid.NewGuid().ToString();
private readonly string clientId = "client";
@ -46,10 +47,13 @@ namespace Squidex.Domain.Apps.Entities.Apps
public AppGrainTests()
{
A.CallTo(() => appProvider.GetAppAsync(AppName))
.Returns((IAppEntity)null);
.Returns((IAppEntity)null);
A.CallTo(() => userResolver.FindByIdAsync(contributorId))
.Returns(A.Fake<IUser>());
A.CallTo(() => user.Id)
.Returns(contributorId);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId))
.Returns(user);
initialPatterns = new InitialPatterns
{
@ -163,7 +167,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(5));
result.ShouldBeEquivalent(EntityCreatedResult.Create(contributorId, 5));
Assert.Equal(AppContributorPermission.Editor, sut.Snapshot.Contributors[contributorId]);

34
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs

@ -20,14 +20,29 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
public class GuardAppContributorsTests
{
private readonly IUser user1 = A.Fake<IUser>();
private readonly IUser user2 = A.Fake<IUser>();
private readonly IUser user3 = A.Fake<IUser>();
private readonly IUserResolver users = A.Fake<IUserResolver>();
private readonly IAppLimitsPlan appPlan = A.Fake<IAppLimitsPlan>();
private readonly AppContributors contributors_0 = AppContributors.Empty;
public GuardAppContributorsTests()
{
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored))
.Returns(A.Fake<IUser>());
A.CallTo(() => user1.Id).Returns("1");
A.CallTo(() => user2.Id).Returns("2");
A.CallTo(() => user3.Id).Returns("3");
A.CallTo(() => users.FindByIdOrEmailAsync("1")).Returns(user1);
A.CallTo(() => users.FindByIdOrEmailAsync("2")).Returns(user2);
A.CallTo(() => users.FindByIdOrEmailAsync("3")).Returns(user3);
A.CallTo(() => users.FindByIdOrEmailAsync("1@email.com")).Returns(user1);
A.CallTo(() => users.FindByIdOrEmailAsync("2@email.com")).Returns(user2);
A.CallTo(() => users.FindByIdOrEmailAsync("3@email.com")).Returns(user3);
A.CallTo(() => users.FindByIdOrEmailAsync("notfound"))
.Returns(Task.FromResult<IUser>(null));
A.CallTo(() => appPlan.MaxContributors)
.Returns(10);
@ -62,10 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact]
public Task CanAssign_should_throw_exception_if_user_not_found()
{
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored))
.Returns(Task.FromResult<IUser>(null));
var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 };
var command = new AssignContributor { ContributorId = "notfound", Permission = (AppContributorPermission)10 };
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan));
}
@ -84,6 +96,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan));
}
[Fact]
public async Task CanAssign_assign_if_if_user_added_by_email()
{
var command = new AssignContributor { ContributorId = "1@email.com" };
await GuardAppContributors.CanAssign(contributors_0, command, users, appPlan);
Assert.Equal("1", command.ContributorId);
}
[Fact]
public Task CanAssign_should_not_throw_exception_if_user_found()
{

2
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs

@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
A.CallTo(() => apps.GetAppAsync("new-app"))
.Returns(Task.FromResult<IAppEntity>(null));
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored))
A.CallTo(() => users.FindByIdOrEmailAsync(A<string>.Ignored))
.Returns(A.Fake<IUser>());
A.CallTo(() => appPlans.GetPlan("free"))

Loading…
Cancel
Save