Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error handling of changed receipt name #12560

Merged
merged 3 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 23 additions & 97 deletions backend/src/Designer/Controllers/AppDevelopmentController.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Runtime.Serialization;

namespace Altinn.Studio.Designer.Exceptions
namespace Altinn.Studio.Designer.Exceptions.AppDevelopment
{
/// <summary>
/// Indicates that an error occurred during C# code generation.
Expand All @@ -23,11 +22,5 @@ public EmptyLayoutSetIdException(string message) : base(message)
public EmptyLayoutSetIdException(string message, Exception innerException) : base(message, innerException)
{
}

/// <inheritdoc/>
protected EmptyLayoutSetIdException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;

namespace Altinn.Studio.Designer.Exceptions.AppDevelopment
{
/// <summary>
/// Indicates that an error occurred during C# code generation.
/// </summary>
[Serializable]
public class NoLayoutSetsFileFoundException : Exception
{
/// <inheritdoc/>
public NoLayoutSetsFileFoundException()
{
}

/// <inheritdoc/>
public NoLayoutSetsFileFoundException(string message) : base(message)
{
}

/// <inheritdoc/>
public NoLayoutSetsFileFoundException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;

namespace Altinn.Studio.Designer.Exceptions.AppDevelopment
{
/// <summary>
/// Indicates that an error occurred during C# code generation.
/// </summary>
[Serializable]
public class NonUniqueLayoutSetIdException : Exception
{
/// <inheritdoc/>
public NonUniqueLayoutSetIdException()
{
}

/// <inheritdoc/>
public NonUniqueLayoutSetIdException(string message) : base(message)
{
}

/// <inheritdoc/>
public NonUniqueLayoutSetIdException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
33 changes: 33 additions & 0 deletions backend/src/Designer/Exceptions/InvalidLayoutSetIdException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Runtime.Serialization;

namespace Altinn.Studio.Designer.Exceptions
{
/// <summary>
/// Indicates that an error occurred during C# code generation.
/// </summary>
[Serializable]
public class InvalidLayoutSetIdException : Exception
{
/// <inheritdoc/>
public InvalidLayoutSetIdException()
{
}

/// <inheritdoc/>
public InvalidLayoutSetIdException(string message) : base(message)
{
}

/// <inheritdoc/>
public InvalidLayoutSetIdException(string message, Exception innerException) : base(message, innerException)
{
}

/// <inheritdoc/>
protected InvalidLayoutSetIdException(SerializationInfo info, StreamingContext context) : base(info, context)

Check warning on line 28 in backend/src/Designer/Exceptions/InvalidLayoutSetIdException.cs

View workflow job for this annotation

GitHub Actions / Run integration tests against actual gitea

'Exception.Exception(SerializationInfo, StreamingContext)' is obsolete: 'This API supports obsolete formatter-based serialization. It should not be called or extended by application code.' (https://aka.ms/dotnet-warnings/SYSLIB0051)
{
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public override void OnException(ExceptionContext context)
{
context.Result = new ObjectResult(ProblemDetailsUtils.GenerateProblemDetails(context.Exception, AppDevelopmentErrorCodes.NonUniqueLayoutSetIdError, HttpStatusCode.BadRequest)) { StatusCode = (int)HttpStatusCode.BadRequest };
}
if (context.Exception is EmptyLayoutSetIdException)
if (context.Exception is InvalidLayoutSetIdException)
{
context.Result = new ObjectResult(ProblemDetailsUtils.GenerateProblemDetails(context.Exception, AppDevelopmentErrorCodes.EmptyLayoutSetIdError, HttpStatusCode.BadRequest)) { StatusCode = (int)HttpStatusCode.BadRequest };
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
using System.IO;
using System.Linq;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Altinn.App.Core.Models;
using Altinn.Studio.DataModeling.Metamodel;
using Altinn.Studio.Designer.Exceptions;
using Altinn.Studio.Designer.Exceptions.AppDevelopment;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Infrastructure.GitRepository;
using Altinn.Studio.Designer.Models;
Expand All @@ -15,6 +17,7 @@
using Microsoft.AspNetCore.Http;
using NuGet.Versioning;
using LayoutSets = Altinn.Studio.Designer.Models.LayoutSets;
using NonUniqueLayoutSetIdException = Altinn.Studio.Designer.Exceptions.NonUniqueLayoutSetIdException;
using PlatformStorageModels = Altinn.Platform.Storage.Interface.Models;

namespace Altinn.Studio.Designer.Services.Implementation
Expand All @@ -26,6 +29,7 @@ public class AppDevelopmentService : IAppDevelopmentService
{
private readonly IAltinnGitRepositoryFactory _altinnGitRepositoryFactory;
private readonly ISchemaModelService _schemaModelService;
private readonly string _layoutSetNameRegEx = "[a-zA-Z0-9-]{2,28}";

/// <summary>
/// Constructor
Expand Down Expand Up @@ -198,7 +202,7 @@ public async Task<ModelMetadata> GetModelMetadata(AltinnRepoEditingContext altin

private string GetModelName(ApplicationMetadata applicationMetadata, [CanBeNull] string taskId)
{
// fallback to first model if no task_id is provided (no layoutsets)
// fallback to first model if no task_id is provided (no layout sets)
if (taskId == null)
{
return applicationMetadata.DataTypes.FirstOrDefault(data => data.AppLogic != null && !string.IsNullOrEmpty(data.AppLogic.ClassRef) && !string.IsNullOrEmpty(data.TaskId))?.Id ?? string.Empty;
Expand All @@ -217,7 +221,13 @@ private bool DoesDataTaskMatchTaskId(PlatformStorageModels.DataType data, [CanBe

private async Task<string> GetTaskIdBasedOnLayoutSet(AltinnRepoEditingContext altinnRepoEditingContext, string layoutSetName, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(layoutSetName))
{
// App without layout sets --> no need for task_id, we just retrieve the first occurence of a dataType with a classRef
return null;
}
LayoutSets layoutSets = await GetLayoutSets(altinnRepoEditingContext, cancellationToken);

return layoutSets?.Sets?.Find(set => set.Id == layoutSetName)?.Tasks[0];
}

Expand All @@ -237,7 +247,8 @@ public async Task<LayoutSets> GetLayoutSets(AltinnRepoEditingContext altinnRepoE
return layoutSets;
}

return null;
throw new NoLayoutSetsFileFoundException(
"No layout set found for this app.");
}

/// <inheritdoc />
Expand All @@ -250,11 +261,11 @@ public async Task<LayoutSets> AddLayoutSet(AltinnRepoEditingContext altinnRepoEd
altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);
if (!altinnAppGitRepository.AppUsesLayoutSets())
{
throw new FileNotFoundException("No layout set found for this app");
throw new NoLayoutSetsFileFoundException("No layout set found for this app.");
}
if (string.IsNullOrEmpty(newLayoutSet.Id))
if (!Regex.IsMatch(newLayoutSet.Id, _layoutSetNameRegEx))
{
throw new EmptyLayoutSetIdException("New layout set name must have a value.");
throw new InvalidLayoutSetIdException("New layout set name is not valid.");
}
LayoutSets layoutSets = await altinnAppGitRepository.GetLayoutSetsFile(cancellationToken);
if (layoutSets.Sets.Exists(set => set.Id == newLayoutSet.Id))
Expand All @@ -275,23 +286,25 @@ public async Task<LayoutSets> UpdateLayoutSet(AltinnRepoEditingContext altinnRep
altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);
if (!altinnAppGitRepository.AppUsesLayoutSets())
{
throw new FileNotFoundException("No layout set found for this app");

throw new NoLayoutSetsFileFoundException("No layout set found for this app.");
}
if (string.IsNullOrEmpty(newLayoutSet.Id))
if (!Regex.IsMatch(newLayoutSet.Id, _layoutSetNameRegEx))
{
throw new EmptyLayoutSetIdException("New layout set name must have a value.");
throw new InvalidLayoutSetIdException("New layout set name is not valid.");
}
LayoutSets layoutSets = await altinnAppGitRepository.GetLayoutSetsFile(cancellationToken);
LayoutSetConfig layoutSetToReplace = layoutSets.Sets.Find(set => set.Id == layoutSetToUpdateId);
if (newLayoutSet.Id == layoutSetToUpdateId)
{
return await UpdateExistingLayoutSet(altinnAppGitRepository, layoutSets, layoutSetToReplace, newLayoutSet);
}
// Layout set name is updated which means layout set folder must be updated also
// NewLayoutSet Id is not the same as existing layout set Id so must check if the suggested new Id already exists
if (layoutSets.Sets.Exists(set => set.Id == newLayoutSet.Id))
{
throw new NonUniqueLayoutSetIdException($"Layout set name, {newLayoutSet.Id}, already exists.");
}
// Layout set name is updated which means layout set folder must be updated also
altinnAppGitRepository.ChangeLayoutSetFolderName(layoutSetToUpdateId, newLayoutSet.Id, cancellationToken);
return await UpdateExistingLayoutSet(altinnAppGitRepository, layoutSets, layoutSetToReplace, newLayoutSet);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public Task<ModelMetadata> GetModelMetadata(
public Task<LayoutSets> GetLayoutSets(AltinnRepoEditingContext altinnRepoEditingContext, CancellationToken cancellationToken = default);

/// <summary>
/// Adds a config for an additional layout set to the layout-set.json
/// Adds a config for an additional layout set to the layout-sets.json
/// </summary>
/// <param name="altinnRepoEditingContext">An <see cref="AltinnRepoEditingContext"/>.</param>
/// <param name="newLayoutSet">Config for the new layout set</param>
Expand All @@ -107,7 +107,7 @@ public Task<ModelMetadata> GetModelMetadata(
/// </summary>
/// <param name="altinnRepoEditingContext">An <see cref="AltinnRepoEditingContext"/>.</param>
/// <param name="layoutSetToUpdateId">The id of the layout set to replace</param>
/// <param name="newLayoutSet">Config for the new layout set</param>
/// <param name="newLayoutSet">Config for the updated layout set</param>
/// <param name="cancellationToken">An <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
public Task<LayoutSets> UpdateLayoutSet(AltinnRepoEditingContext altinnRepoEditingContext, string layoutSetToUpdateId, LayoutSetConfig newLayoutSet, CancellationToken cancellationToken = default);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.IO;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Exceptions;
using Altinn.Studio.Designer.Exceptions.AppDevelopment;
using Altinn.Studio.Designer.Factories;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Implementation;
Expand All @@ -12,6 +12,7 @@
using FluentAssertions;
using Moq;
using Xunit;
using NonUniqueLayoutSetIdException = Altinn.Studio.Designer.Exceptions.NonUniqueLayoutSetIdException;

namespace Designer.Tests.Services;

Expand Down Expand Up @@ -180,7 +181,7 @@ public async Task AddLayoutSet_WhenLayoutSetDoesNotExist_ShouldAddNewLayoutSet()
// Assert
updatedLayoutSets.Should().NotBeNull();
updatedLayoutSets.Sets.Should().HaveCount(4);
updatedLayoutSets.Sets.Should().Contain(newLayoutSet); // Ensure newLayoutSet is added
updatedLayoutSets.Sets.Should().Contain(newLayoutSet);
}

[Fact]
Expand Down Expand Up @@ -212,7 +213,7 @@ public async Task UpdateLayoutSet_WhenAppHasNoLayoutSets_ShouldThrowFileNotFound
Func<Task> act = async () => await _appDevelopmentService.UpdateLayoutSet(AltinnRepoEditingContext.FromOrgRepoDeveloper(_org, targetRepository, _developer), "layoutSet1", new LayoutSetConfig());

// Assert
await act.Should().ThrowAsync<FileNotFoundException>();
await act.Should().ThrowAsync<NoLayoutSetsFileFoundException>();
}

[Fact]
Expand All @@ -228,7 +229,7 @@ public async Task AddLayoutSet_WhenAppHasNoLayoutSets_ShouldThrowFileNotFoundExc
Func<Task> act = async () => await _appDevelopmentService.AddLayoutSet(AltinnRepoEditingContext.FromOrgRepoDeveloper(_org, targetRepository, _developer), new LayoutSetConfig());

// Assert
await act.Should().ThrowAsync<FileNotFoundException>();
await act.Should().ThrowAsync<NoLayoutSetsFileFoundException>();
}

private List<string> GetFileNamesInLayoutSet(string layoutSetName)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,19 @@
import React from 'react';
import { act, screen } from '@testing-library/react';
import { screen } from '@testing-library/react';
import { ProcessEditor } from './ProcessEditor';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { renderWithProviders } from '../../test/testUtils';
import { QueryKey } from 'app-shared/types/QueryKey';
import type { AppVersion } from 'app-shared/types/AppVersion';
import { textMock } from '../../../testing/mocks/i18nMock';
import { APP_DEVELOPMENT_BASENAME, PROTECTED_TASK_NAME_CUSTOM_RECEIPT } from 'app-shared/constants';
import { APP_DEVELOPMENT_BASENAME } from 'app-shared/constants';
import { useBpmnContext } from '../../../packages/process-editor/src/contexts/BpmnContext';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import userEvent from '@testing-library/user-event';
import { layoutSets } from 'app-shared/mocks/mocks';
import { layoutSetsMock } from '../../../packages/ux-editor/src/testing/layoutMock';
import type { LayoutSetConfig } from 'app-shared/types/api/LayoutSetsResponse';

// test data
const org = 'org';
const app = 'app';
const defaultAppVersion: AppVersion = { backendVersion: '8.0.0', frontendVersion: '4.0.0' };

jest.mock('app-shared/hooks/useConfirmationDialogOnPageLeave', () => ({
useConfirmationDialogOnPageLeave: jest.fn(),
}));

jest.mock('../../../packages/process-editor/src/contexts/BpmnContext', () => ({
...jest.requireActual('../../../packages/process-editor/src/contexts/BpmnContext'),
useBpmnContext: jest.fn(),
Expand Down Expand Up @@ -69,68 +60,6 @@ describe('ProcessEditor', () => {
renderProcessEditor({ bpmnFile: 'mockBpmn', queryClient: queryClientMock });
screen.getByText(textMock('process_editor.configuration_panel_end_event'));
});

it('calls onUpdateLayoutSet and trigger addLayoutSet mutation call when layoutSetName for custom receipt is added', async () => {
const customReceiptLayoutSetName = 'CustomReceipt';
const user = userEvent.setup();
const queryClientMock = createQueryClientMock();
queryClientMock.setQueryData([QueryKey.AppVersion, org, app], defaultAppVersion);
(useBpmnContext as jest.Mock).mockReturnValue({
bpmnDetails: { type: 'bpmn:EndEvent' },
isEditAllowed: true,
});
renderProcessEditor({ bpmnFile: 'mockBpmn', queryClient: queryClientMock });
const inputFieldButton = screen.getByTitle(
textMock('process_editor.configuration_panel_custom_receipt_add'),
);
await act(() => user.click(inputFieldButton));
const inputField = screen.getByTitle(
textMock('process_editor.configuration_panel_custom_receipt_add_button_title'),
);
await act(() => user.type(inputField, customReceiptLayoutSetName));
await act(() => user.tab());
expect(queriesMock.addLayoutSet).toHaveBeenCalledTimes(1);
expect(queriesMock.addLayoutSet).toHaveBeenCalledWith(org, app, customReceiptLayoutSetName, {
id: customReceiptLayoutSetName,
tasks: [PROTECTED_TASK_NAME_CUSTOM_RECEIPT],
});
});

it('calls onUpdateLayoutSet and trigger updateLayoutSet mutation call when layoutSetName for custom receipt is changed', async () => {
const customReceiptLayoutSetName = 'CustomReceipt';
const newCustomReceiptLayoutSetName = 'NewCustomReceipt';
const user = userEvent.setup();
const queryClientMock = createQueryClientMock();
queryClientMock.setQueryData([QueryKey.AppVersion, org, app], defaultAppVersion);
const layoutSetsWithCustomReceipt: LayoutSetConfig[] = [
...layoutSets.sets,
{ id: customReceiptLayoutSetName, tasks: [PROTECTED_TASK_NAME_CUSTOM_RECEIPT] },
];
queryClientMock.setQueryData([QueryKey.LayoutSets, org, app], {
...layoutSetsMock,
sets: layoutSetsWithCustomReceipt,
});
(useBpmnContext as jest.Mock).mockReturnValue({
bpmnDetails: { type: 'bpmn:EndEvent' },
isEditAllowed: true,
});
renderProcessEditor({ bpmnFile: 'mockBpmn', queryClient: queryClientMock });
const inputFieldButton = screen.getByTitle(
textMock('process_editor.configuration_panel_custom_receipt_add'),
);
await act(() => user.click(inputFieldButton));
const inputField = screen.getByTitle(
textMock('process_editor.configuration_panel_custom_receipt_add_button_title'),
);
await act(() => user.clear(inputField));
await act(() => user.type(inputField, newCustomReceiptLayoutSetName));
await act(() => user.tab());
expect(queriesMock.updateLayoutSet).toHaveBeenCalledTimes(1);
expect(queriesMock.updateLayoutSet).toHaveBeenCalledWith(org, app, customReceiptLayoutSetName, {
id: newCustomReceiptLayoutSetName,
tasks: [PROTECTED_TASK_NAME_CUSTOM_RECEIPT],
});
});
});

const renderProcessEditor = ({ bpmnFile = null, queryClient = createQueryClientMock() } = {}) => {
Expand Down
Loading
Loading