Browse Source

Feature/contributor import (#409)

* Variable naming fixed.

* Tests improved and contributor import dialog.
pull/407/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
42e6b3feab
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs
  2. 44
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  3. 26
      src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs
  4. 8
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  5. 5
      src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs
  6. 2
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.scss
  7. 1
      src/Squidex/app/features/settings/declarations.ts
  8. 2
      src/Squidex/app/features/settings/module.ts
  9. 10
      src/Squidex/app/features/settings/pages/backups/backups-page.component.html
  10. 29
      src/Squidex/app/features/settings/pages/backups/backups-page.component.scss
  11. 10
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  12. 15
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  13. 43
      src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.html
  14. 10
      src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.scss
  15. 96
      src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts
  16. 9
      src/Squidex/app/framework/angular/forms/form-hint.component.ts
  17. 14
      src/Squidex/app/framework/angular/status-icon.component.html
  18. 41
      src/Squidex/app/framework/angular/status-icon.component.scss
  19. 25
      src/Squidex/app/framework/angular/status-icon.component.ts
  20. 7
      src/Squidex/app/framework/declarations.ts
  21. 3
      src/Squidex/app/framework/module.ts
  22. 60
      src/Squidex/app/shared/state/contributors.forms.ts
  23. 4
      src/Squidex/app/shared/state/contributors.state.ts
  24. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs
  25. 48
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs
  26. 7
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs
  27. 13
      tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs
  28. 15
      tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs
  29. 15
      tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs
  30. 19
      tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs

6
src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs

@ -13,12 +13,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public string ContributorId { get; set; }
public string Role { get; set; } = Roles.Developer;
public string Role { get; set; } = Roles.Editor;
public bool IsRestore { get; set; }
public bool IsInviting { get; set; }
public bool IsCreated { get; set; }
public bool Invite { get; set; }
}
}

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

@ -32,35 +32,39 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
if (string.IsNullOrWhiteSpace(command.ContributorId))
{
e(Not.Defined("Contributor id"), nameof(command.ContributorId));
return;
}
var user = await users.FindByIdOrEmailAsync(command.ContributorId);
if (user == null)
else
{
throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity));
}
command.ContributorId = user.Id;
var user = await users.FindByIdOrEmailAsync(command.ContributorId);
if (!command.IsRestore)
{
if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase))
if (user == null)
{
throw new DomainForbiddenException("You cannot change your own role.");
throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity));
}
if (contributors.TryGetValue(command.ContributorId, out var existing))
command.ContributorId = user.Id;
if (!command.IsRestore)
{
if (existing == command.Role)
if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase))
{
e(Not.New("Contributor", "role"), nameof(command.Role));
throw new DomainForbiddenException("You cannot change your own role.");
}
if (contributors.TryGetValue(command.ContributorId, out var role))
{
if (role == command.Role)
{
e(Not.New("Contributor", "role"), nameof(command.Role));
}
}
else
{
if (plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors)
{
e("You have reached the maximum number of contributors for your plan.");
}
}
}
else if (plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors)
{
e("You have reached the maximum number of contributors for your plan.");
}
}
});

26
src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs

@ -27,24 +27,26 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (context.Command is AssignContributor assignContributor)
if (context.Command is AssignContributor assignContributor && ShouldInvite(assignContributor))
{
if (assignContributor.IsInviting && assignContributor.ContributorId.IsEmail())
{
assignContributor.IsCreated = await userResolver.CreateUserIfNotExists(assignContributor.ContributorId, true);
await next();
var created = await userResolver.CreateUserIfNotExists(assignContributor.ContributorId, true);
if (assignContributor.IsCreated && context.PlainResult is IAppEntity app)
{
context.Complete(new InvitedResult { App = app });
}
await next();
return;
if (created && context.PlainResult is IAppEntity app)
{
context.Complete(new InvitedResult { App = app });
}
}
else
{
await next();
}
}
await next();
private bool ShouldInvite(AssignContributor assignContributor)
{
return assignContributor.Invite && assignContributor.ContributorId.IsEmail();
}
}
}

8
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -52,8 +52,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
switch (command)
{
case CreateAsset createRule:
return CreateReturnAsync(createRule, async c =>
case CreateAsset createAsset:
return CreateReturnAsync(createAsset, async c =>
{
GuardAsset.CanCreate(c);
@ -63,8 +63,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
return Snapshot;
});
case UpdateAsset updateRule:
return UpdateReturn(updateRule, c =>
case UpdateAsset updateAsset:
return UpdateReturn(updateAsset, c =>
{
GuardAsset.CanUpdate(c);

5
src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs

@ -8,6 +8,7 @@
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
using Roles = Squidex.Domain.Apps.Core.Apps.Role;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -22,7 +23,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// <summary>
/// The role of the contributor.
/// </summary>
public string Role { get; set; }
public string Role { get; set; } = Roles.Developer;
/// <summary>
/// Set to true to invite the user if he does not exist.
@ -31,7 +32,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public AssignContributor ToCommand()
{
return SimpleMapper.Map(this, new AssignContributor { IsInviting = Invite });
return SimpleMapper.Map(this, new AssignContributor());
}
}
}

2
src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.scss

@ -1,6 +1,6 @@
@import '_vars';
@import '_mixins';
texarea {
textarea {
resize: none;
}

1
src/Squidex/app/features/settings/declarations.ts

@ -10,6 +10,7 @@ export * from './pages/backups/pipes';
export * from './pages/clients/client.component';
export * from './pages/clients/clients-page.component';
export * from './pages/contributors/contributors-page.component';
export * from './pages/contributors/import-contributors-dialog.component';
export * from './pages/languages/language.component';
export * from './pages/languages/languages-page.component';
export * from './pages/more/more-page.component';

2
src/Squidex/app/features/settings/module.ts

@ -21,6 +21,7 @@ import {
ClientComponent,
ClientsPageComponent,
ContributorsPageComponent,
ImportContributorsDialogComponent,
LanguageComponent,
LanguagesPageComponent,
MorePageComponent,
@ -203,6 +204,7 @@ const routes: Routes = [
ClientComponent,
ClientsPageComponent,
ContributorsPageComponent,
ImportContributorsDialogComponent,
LanguageComponent,
LanguagesPageComponent,
MorePageComponent,

10
src/Squidex/app/features/settings/pages/backups/backups-page.component.html

@ -34,15 +34,7 @@
<div class="table-items-row" *ngFor="let backup of backups; trackBy: trackByBackup">
<div class="row">
<div class="col-auto" [ngSwitch]="backup.status">
<div *ngSwitchCase="'Failed'" class="backup-status backup-status-failed">
<i class="icon-exclamation"></i>
</div>
<div *ngSwitchCase="'Completed'" class="backup-status backup-status-success">
<i class="icon-checkmark"></i>
</div>
<div *ngSwitchDefault class="backup-status backup-status-pending spin">
<i class="icon-hour-glass"></i>
</div>
<sqx-status-icon size="lg" [status]="backup.status"></sqx-status-icon>
</div>
<div class="col-auto">
<div>

29
src/Squidex/app/features/settings/pages/backups/backups-page.component.scss

@ -1,29 +1,2 @@
@import '_vars';
@import '_mixins';
$circle-size: 2.8rem;
.backup-status {
& {
@include circle($circle-size);
line-height: $circle-size + .1rem;
text-align: center;
font-size: .4 * $circle-size;
font-weight: normal;
background: $color-border;
color: $color-dark-foreground;
vertical-align: middle;
}
&-pending {
color: inherit;
}
&-failed {
background: $color-theme-error;
}
&-success {
background: $color-theme-green;
}
}
@import '_mixins';

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

@ -74,6 +74,10 @@
</div>
</form>
</div>
<sqx-form-hint class="text-right">
Big team? <a class="force" (click)="importDialog.show()">Hide many contributors at once</a>
</sqx-form-hint>
</ng-container>
</ng-container>
</ng-container>
@ -92,4 +96,10 @@
</ng-container>
</sqx-panel>
<ng-container *sqxModal="importDialog">
<sqx-import-contributors-dialog
(close)="importDialog.hide()">
</sqx-import-contributors-dialog>
</ng-container>
<router-outlet></router-outlet>

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

@ -16,10 +16,9 @@ import {
AutocompleteSource,
ContributorDto,
ContributorsState,
DialogModel,
DialogService,
RolesState,
Types,
UserDto,
UsersService
} from '@app/shared';
@ -57,6 +56,8 @@ export class UsersDataSource implements AutocompleteSource {
export class ContributorsPageComponent implements OnInit {
public assignContributorForm = new AssignContributorForm(this.formBuilder);
public importDialog = new DialogModel();
constructor(
public readonly appsState: AppsState,
public readonly contributorsState: ContributorsState,
@ -89,15 +90,7 @@ export class ContributorsPageComponent implements OnInit {
const value = this.assignContributorForm.submit();
if (value) {
let user = value.user;
if (Types.is(user, UserDto)) {
user = user.id;
}
const requestDto = { contributorId: user, role: 'Editor', invite: true };
this.contributorsState.assign(requestDto)
this.contributorsState.assign(value)
.subscribe(isCreated => {
this.assignContributorForm.submitCompleted();

43
src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.html

@ -0,0 +1,43 @@
<form [formGroup]="importForm.form" (ngSubmit)="import()">
<sqx-modal-dialog (close)="emitClose()">
<ng-container title>
Import contributors
</ng-container>
<ng-container content>
<div class="content">
<ng-container *ngIf="importStatus; else noStatus">
<div class="row pb-2" *ngFor="let status of importStatus">
<div class="col truncate">
{{status.email}}
</div>
<div class="col-auto">
<sqx-status-icon size="sm"
[status]="status.result"
[statusText]="status.resultText">
</sqx-status-icon>
</div>
</div>
</ng-container>
<ng-template #noStatus>
<textarea class="form-control content" placeholder="user1@squidex.io;user2.squidex.io" formControlName="import"></textarea>
</ng-template>
</div>
<sqx-form-hint>
<ng-container *ngIf="importForm.numberOfEmails | async; let emails">
Emails detected: {{emails}}
</ng-container>
&nbsp;
</sqx-form-hint>
</ng-container>
<ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="emitClose()">Cancel</button>
<button type="submit" class="float-right btn btn-success" [disabled]="importStatus || (importForm.hasNoUser | async)">Add Contributors</button>
</ng-container>
</sqx-modal-dialog>
</form>

10
src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.scss

@ -0,0 +1,10 @@
@import '_vars';
@import '_mixins';
textarea {
resize: none;
}
.content {
min-height: 300px;
}

96
src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts

@ -0,0 +1,96 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { empty, of } from 'rxjs';
import { catchError, mergeMap, tap } from 'rxjs/operators';
import {
ContributorsState,
ErrorDto,
ImportContributorsForm
} from '@app/shared';
interface ImportStatus {
email: string;
result: 'Pending' | 'Failed' | 'Success';
resultText: string;
}
@Component({
selector: 'sqx-import-contributors-dialog',
styleUrls: ['./import-contributors-dialog.component.scss'],
templateUrl: './import-contributors-dialog.component.html'
})
export class ImportContributorsDialogComponent {
@Output()
public close = new EventEmitter();
public importForm = new ImportContributorsForm(this.formBuilder);
public importStatus: ImportStatus[];
constructor(
private readonly formBuilder: FormBuilder,
private readonly contributorsState: ContributorsState
) {
}
public import() {
const contributors = this.importForm.submit();
if (contributors && contributors.length > 0) {
this.importStatus = [];
for (let contributor of contributors) {
this.importStatus.push({
email: contributor.contributorId,
result: 'Pending',
resultText: 'Pending'
});
}
of(...contributors).pipe(
mergeMap(c =>
this.contributorsState.assign(c, { silent: true }).pipe(
tap(created => {
let status = this.importStatus.find(x => x.email === c.contributorId);
if (status) {
status.resultText = getSuccess(created);
status.result = 'Success';
}
}),
catchError((error: ErrorDto) => {
let status = this.importStatus.find(x => x.email === c.contributorId);
if (status) {
status.resultText = getError(error);
status.result = 'Failed';
}
return empty();
})
), 1)
).subscribe();
}
}
public emitClose() {
this.close.emit();
}
}
function getError(error: ErrorDto): string {
return error.details[0];
}
function getSuccess(created: boolean | undefined): string {
return created ?
'User has been invited and assigned.' :
'User has been assigned';
}

9
src/Squidex/app/framework/angular/forms/form-hint.component.ts

@ -5,14 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'sqx-form-hint',
template: `
<small class="text-muted form-text">
<small class="text-muted form-text {{class}}">
<ng-content></ng-content>
</small>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FormHintComponent {}
export class FormHintComponent {
@Input()
public class: string;
}

14
src/Squidex/app/framework/angular/status-icon.component.html

@ -0,0 +1,14 @@
<ng-container [ngSwitch]="status">
<div *ngSwitchCase="'Failed'" class="status status-failed {{size}}" [title]="statusText">
<i class="icon-exclamation"></i>
</div>
<div *ngSwitchCase="'Success'" class="status status-success {{size}}" [title]="statusText">
<i class="icon-checkmark"></i>
</div>
<div *ngSwitchCase="'Completed'" class="status status-success {{size}}" [title]="statusText">
<i class="icon-checkmark"></i>
</div>
<div *ngSwitchDefault class="status status-pending spin {{size}}" [title]="statusText">
<i class="icon-hour-glass"></i>
</div>
</ng-container>

41
src/Squidex/app/framework/angular/status-icon.component.scss

@ -0,0 +1,41 @@
@import '_mixins';
@import '_vars';
$circle-size-sm: 1.6rem;
$circle-size-lg: 2.8rem;
.status {
& {
text-align: center;
background: $color-border;
color: $color-dark-foreground;
vertical-align: middle;
}
&.sm {
@include circle($circle-size-sm);
line-height: $circle-size-sm + .1rem;
font-size: .5 * $circle-size-sm;
font-weight: normal;
}
&.lg {
@include circle($circle-size-lg);
line-height: $circle-size-lg + .1rem;
font-size: .5 * $circle-size-lg;
font-weight: normal;
}
&-pending {
color: inherit;
}
&-failed {
background: $color-theme-error;
}
&-success {
background: $color-theme-green;
}
}

25
src/Squidex/app/framework/angular/status-icon.component.ts

@ -0,0 +1,25 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'sqx-status-icon',
styleUrls: ['./status-icon.component.scss'],
templateUrl: './status-icon.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class StatusIconComponent {
@Input()
public status: 'Started' | 'Failed' | 'Success' | 'Completed' | 'Failed' | 'Pending';
@Input()
public statusText: string;
@Input()
public size: 'lg' | 'sm' = 'lg';
}

7
src/Squidex/app/framework/declarations.ts

@ -55,18 +55,19 @@ export * from './angular/routers/parent-link.directive';
export * from './angular/code.component';
export * from './angular/external-link.directive';
export * from './angular/hover-background.directive';
export * from './angular/highlight.pipe';
export * from './angular/hover-background.directive';
export * from './angular/ignore-scrollbar.directive';
export * from './angular/image-source.directive';
export * from './angular/panel.component';
export * from './angular/panel-container.directive';
export * from './angular/pager.component';
export * from './angular/panel-container.directive';
export * from './angular/panel.component';
export * from './angular/popup-link.directive';
export * from './angular/safe-html.pipe';
export * from './angular/scroll-active.directive';
export * from './angular/shortcut.component';
export * from './angular/sorted.directive';
export * from './angular/status-icon.component';
export * from './angular/stop-click.directive';
export * from './angular/sync-scrolling.directive';
export * from './angular/template-wrapper.directive';

3
src/Squidex/app/framework/module.ts

@ -82,6 +82,7 @@ import {
ShortTimePipe,
SortedDirective,
StarsComponent,
StatusIconComponent,
StopClickDirective,
SyncScollingDirective,
TagEditorComponent,
@ -159,6 +160,7 @@ import {
ShortTimePipe,
SortedDirective,
StarsComponent,
StatusIconComponent,
StopClickDirective,
SyncScollingDirective,
TagEditorComponent,
@ -230,6 +232,7 @@ import {
ShortTimePipe,
SortedDirective,
StarsComponent,
StatusIconComponent,
StopClickDirective,
SyncScollingDirective,
TagEditorComponent,

60
src/Squidex/app/shared/state/contributors.forms.ts

@ -6,12 +6,20 @@
*/
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { map } from 'rxjs/operators';
import { Form, hasNoValue$ } from '@app/framework';
import {
Form,
hasNoValue$,
Types,
value$
} from '@app/framework';
import { AssignContributorDto } from '../services/contributors.service';
import { UserDto } from './../services/users.service';
export class AssignContributorForm extends Form<FormGroup, { user: string | UserDto }> {
export class AssignContributorForm extends Form<FormGroup, AssignContributorDto> {
public hasNoUser = hasNoValue$(this.form.controls['user']);
constructor(formBuilder: FormBuilder) {
@ -23,4 +31,50 @@ export class AssignContributorForm extends Form<FormGroup, { user: string | User
]
}));
}
}
protected transformSubmit(value: string | UserDto) {
if (Types.is(value, UserDto)) {
value = value.id;
}
return { contributorId: value, role: 'Editor', invite: true };
}
}
export class ImportContributorsForm extends Form<FormGroup, AssignContributorDto[]> {
public numberOfEmails = value$(this.form.controls['import']).pipe(map(v => extractEmails(v).length));
public hasNoUser = this.numberOfEmails.pipe(map(v => v === 0));
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
import: ['',
[
Validators.required
]
]
}));
}
protected transformSubmit(value: { import: string }) {
return extractEmails(value.import);
}
}
function extractEmails(value: string) {
let result: AssignContributorDto[] = [];
if (value) {
let emails = value.match(EMAIL_REGEX);
if (emails) {
for (let match of emails) {
result.push({ contributorId: match, role: 'Editor', invite: true });
}
}
}
return result;
}
const EMAIL_REGEX = /(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*/gim;

4
src/Squidex/app/shared/state/contributors.state.ts

@ -94,7 +94,7 @@ export class ContributorsState extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public assign(request: AssignContributorDto): Observable<boolean | undefined> {
public assign(request: AssignContributorDto, options?: { silent: boolean }): Observable<boolean | undefined> {
return this.contributorsService.postContributor(this.appName, request, this.version).pipe(
catchError(error => {
if (Types.is(error, ErrorDto) && error.statusCode === 404) {
@ -106,7 +106,7 @@ export class ContributorsState extends State<Snapshot> {
tap(({ version, payload }) => {
this.replaceContributors(version, payload);
}),
shareMapSubscribed(this.dialogs, x => x.payload._meta && x.payload._meta['isInvited'] === '1'));
shareMapSubscribed(this.dialogs, x => x.payload._meta && x.payload._meta['isInvited'] === '1', options));
}
private replaceContributors(version: Version, payload: ContributorsPayload) {

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

@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new AssignContributor { ContributorId = "1" };
var contributors_1 = contributors_0.Assign("1", Role.Editor);
var contributors_1 = contributors_0.Assign("1", Role.Developer);
await GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan);
}
@ -159,8 +159,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var command = new AssignContributor { ContributorId = "1" };
var contributors_1 = contributors_0.Assign("1", Role.Editor);
var contributors_2 = contributors_1.Assign("2", Role.Editor);
var contributors_1 = contributors_0.Assign("1", Role.Developer);
var contributors_2 = contributors_1.Assign("2", Role.Developer);
await GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan);
}

48
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs

@ -20,6 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation
public class InviteUserCommandMiddlewareTests
{
private readonly IUserResolver userResolver = A.Fake<IUserResolver>();
private readonly IAppEntity app = Mocks.App(NamedId.Of(Guid.NewGuid(), "my-app"));
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly InviteUserCommandMiddleware sut;
@ -31,19 +32,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation
[Fact]
public async Task Should_invite_user_and_change_result()
{
var command = new AssignContributor { ContributorId = "me@email.com", IsInviting = true };
var context = new CommandContext(command, commandBus);
var context =
new CommandContext(new AssignContributor { ContributorId = "me@email.com", Invite = true }, commandBus)
.Complete(app);
A.CallTo(() => userResolver.CreateUserIfNotExists("me@email.com", true))
.Returns(true);
var result = Mocks.App(NamedId.Of(Guid.NewGuid(), "my-app"));
context.Complete(result);
await sut.HandleAsync(context);
Assert.Same(context.Result<InvitedResult>().App, result);
Assert.Same(context.Result<InvitedResult>().App, app);
A.CallTo(() => userResolver.CreateUserIfNotExists("me@email.com", true))
.MustHaveHappened();
@ -52,34 +50,50 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation
[Fact]
public async Task Should_invite_user_and_not_change_result_if_not_added()
{
var command = new AssignContributor { ContributorId = "me@email.com", IsInviting = true };
var context = new CommandContext(command, commandBus);
var context =
new CommandContext(new AssignContributor { ContributorId = "me@email.com", Invite = true }, commandBus)
.Complete(app);
A.CallTo(() => userResolver.CreateUserIfNotExists("me@email.com", true))
.Returns(false);
var result = Mocks.App(NamedId.Of(Guid.NewGuid(), "my-app"));
context.Complete(result);
await sut.HandleAsync(context);
Assert.Same(context.Result<IAppEntity>(), result);
Assert.Same(context.Result<IAppEntity>(), app);
A.CallTo(() => userResolver.CreateUserIfNotExists("me@email.com", true))
.MustHaveHappened();
}
[Fact]
public async Task Should_not_calls_user_resolver_if_not_email()
public async Task Should_not_call_user_resolver_if_not_email()
{
var context =
new CommandContext(new AssignContributor { ContributorId = "123", Invite = true }, commandBus)
.Complete(app);
await sut.HandleAsync(context);
A.CallTo(() => userResolver.CreateUserIfNotExists(A<string>.Ignored, A<bool>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_call_user_resolver_if_not_inviting()
{
var command = new AssignContributor { ContributorId = "123", IsInviting = true };
var context = new CommandContext(command, commandBus);
var context =
new CommandContext(new AssignContributor { ContributorId = "123", Invite = false }, commandBus)
.Complete(app);
await sut.HandleAsync(context);
A.CallTo(() => userResolver.CreateUserIfNotExists(A<string>.Ignored, A<bool>.Ignored))
.MustNotHaveHappened();
}
private CommandContext Context(AssignContributor command)
{
return new CommandContext(command, commandBus);
}
}
}

7
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs

@ -38,10 +38,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
[Fact]
public async Task Should_add_schema_to_index_on_create()
{
var command = new CreateSchema { SchemaId = schemaId.Id, Name = schemaId.Name, AppId = appId };
var context = new CommandContext(command, commandBus);
context.Complete();
var context =
new CommandContext(new CreateSchema { SchemaId = schemaId.Id, Name = schemaId.Name, AppId = appId }, commandBus)
.Complete();
await sut.HandleAsync(context);

13
tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs

@ -38,7 +38,7 @@ namespace Squidex.Web.CommandMiddlewares
.Returns(null);
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -51,7 +51,7 @@ namespace Squidex.Web.CommandMiddlewares
httpContext.Request.Headers[HeaderNames.IfMatch] = "13";
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -64,7 +64,7 @@ namespace Squidex.Web.CommandMiddlewares
httpContext.Request.Headers[HeaderNames.IfMatch] = "W/13";
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -75,7 +75,7 @@ namespace Squidex.Web.CommandMiddlewares
public async Task Should_add_etag_header_to_response()
{
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
context.Complete(new EntitySavedResult(17));
@ -83,5 +83,10 @@ namespace Squidex.Web.CommandMiddlewares
Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag]);
}
private CommandContext Ctx(ICommand command)
{
return new CommandContext(command, commandBus);
}
}
}

15
tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs

@ -37,7 +37,7 @@ namespace Squidex.Web.CommandMiddlewares
public async Task Should_throw_security_exception_when_no_subject_or_client_is_found()
{
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await Assert.ThrowsAsync<SecurityException>(() => sut.HandleAsync(context));
}
@ -49,7 +49,7 @@ namespace Squidex.Web.CommandMiddlewares
.Returns(null);
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -62,7 +62,7 @@ namespace Squidex.Web.CommandMiddlewares
httpContext.User = CreatePrincipal(OpenIdClaims.Subject, "me");
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -75,7 +75,7 @@ namespace Squidex.Web.CommandMiddlewares
httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client");
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -88,13 +88,18 @@ namespace Squidex.Web.CommandMiddlewares
httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client");
var command = new CreateContent { Actor = new RefToken("subject", "me") };
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
Assert.Equal(new RefToken("subject", "me"), command.Actor);
}
private CommandContext Ctx(ICommand command)
{
return new CommandContext(command, commandBus);
}
private static ClaimsPrincipal CreatePrincipal(string claimType, string claimValue)
{
var claimsPrincipal = new ClaimsPrincipal();

15
tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs

@ -47,7 +47,7 @@ namespace Squidex.Web.CommandMiddlewares
appContext.App = null;
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.HandleAsync(context));
}
@ -56,7 +56,7 @@ namespace Squidex.Web.CommandMiddlewares
public async Task Should_assign_app_id_and_name_to_app_command()
{
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -67,7 +67,7 @@ namespace Squidex.Web.CommandMiddlewares
public async Task Should_assign_app_id_to_app_self_command()
{
var command = new ChangePlan();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -78,7 +78,7 @@ namespace Squidex.Web.CommandMiddlewares
public async Task Should_not_override_app_id()
{
var command = new ChangePlan { AppId = Guid.NewGuid() };
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -89,11 +89,16 @@ namespace Squidex.Web.CommandMiddlewares
public async Task Should_not_override_app_id_and_name()
{
var command = new CreateContent { AppId = NamedId.Of(Guid.NewGuid(), "other-app") };
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
Assert.NotEqual(appId, command.AppId);
}
private CommandContext Ctx(ICommand command)
{
return new CommandContext(command, commandBus);
}
}
}

19
tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs

@ -72,7 +72,7 @@ namespace Squidex.Web.CommandMiddlewares
actionContext.RouteData.Values["name"] = "other-schema";
var command = new CreateContent { AppId = appId };
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.HandleAsync(context));
}
@ -81,7 +81,7 @@ namespace Squidex.Web.CommandMiddlewares
public async Task Should_do_nothing_when_route_has_no_parameter()
{
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -94,7 +94,7 @@ namespace Squidex.Web.CommandMiddlewares
actionContext.RouteData.Values["name"] = schemaId.Name;
var command = new CreateContent { AppId = appId };
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -107,7 +107,7 @@ namespace Squidex.Web.CommandMiddlewares
actionContext.RouteData.Values["name"] = schemaId.Id;
var command = new CreateContent { AppId = appId };
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -120,7 +120,7 @@ namespace Squidex.Web.CommandMiddlewares
actionContext.RouteData.Values["name"] = schemaId.Name;
var command = new UpdateSchema();
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -131,7 +131,7 @@ namespace Squidex.Web.CommandMiddlewares
public async Task Should_not_override_schema_id()
{
var command = new CreateSchema { SchemaId = Guid.NewGuid() };
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
@ -142,11 +142,16 @@ namespace Squidex.Web.CommandMiddlewares
public async Task Should_not_override_schema_id_and_name()
{
var command = new CreateContent { SchemaId = NamedId.Of(Guid.NewGuid(), "other-schema") };
var context = new CommandContext(command, commandBus);
var context = Ctx(command);
await sut.HandleAsync(context);
Assert.NotEqual(appId, command.AppId);
}
private CommandContext Ctx(ICommand command)
{
return new CommandContext(command, commandBus);
}
}
}

Loading…
Cancel
Save