Skip to content

Commit

Permalink
Feature/10670 import service from altinn 2 (#11384)
Browse files Browse the repository at this point in the history
* adding basic frontend logic

* spll check

* typofix

* Fixed spelling

* Adding style

* Fixed ttd handling

* Adding handling for service

* fixing code

* Fixed cache key

* Changed to post for import

* Trying to set up import

* Trying to fix undefined

* fixing undefined

* updating post

* Cleaning up cod

* Adding tests

* fixing queriesMock

* rollback useGetResourceList

* dotnet format

* Fixed method

* Fixing modal z index and SetupTab

* Fixing feedback fromPR

* Adding ServerCode enum

* Feedback. Added util fo rcommmon org code

---------

Co-authored-by: Rune T. Larsen <rune@hjemmekino.no>
  • Loading branch information
WilliamThorenfeldt and TheTechArch committed Oct 25, 2023
1 parent a553f0f commit 48f9b25
Show file tree
Hide file tree
Showing 38 changed files with 784 additions and 225 deletions.
18 changes: 13 additions & 5 deletions backend/src/Designer/Controllers/ResourceAdminController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public async Task<ActionResult<ServiceResource>> AddResource(string org, [FromBo
return _repository.AddServiceResource(org, resource);
}

[HttpGet]
[HttpPost]
[Route("designer/api/{org}/resources/importresource/{serviceCode}/{serviceEdition}/{environment}")]
public async Task<ActionResult> ImportResource(string org, string serviceCode, int serviceEdition, string environment)
{
Expand Down Expand Up @@ -260,19 +260,27 @@ public async Task<ActionResult<List<EuroVocTerm>>> GetEuroVoc(CancellationToken

[HttpGet]
[Route("designer/api/{org}/resources/altinn2linkservices/{environment}")]
public async Task<ActionResult<List<AvailableService>>> GetAltinn2LinkServices(string org, string enviroment)
public async Task<ActionResult<List<AvailableService>>> GetAltinn2LinkServices(string org, string environment)
{
string cacheKey = "availablelinkservices:" + org;
string cacheKey = "availablelinkservices:" + org + environment;
if (!_memoryCache.TryGetValue(cacheKey, out List<AvailableService> linkServices))
{

List<AvailableService> unfiltered = await _altinn2MetadataClient.AvailableServices(1044, enviroment);
List<AvailableService> unfiltered = await _altinn2MetadataClient.AvailableServices(1044, environment);

var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetPriority(CacheItemPriority.High)
.SetAbsoluteExpiration(new TimeSpan(0, _cacheSettings.DataNorgeApiCacheTimeout, 0));

linkServices = unfiltered.Where(a => a.ServiceType.Equals(ServiceType.Link) && a.ServiceOwnerCode.ToLower().Equals(org.ToLower())).ToList();
if (OrgUtil.IsTestEnv(org))
{
linkServices = unfiltered.Where(a => a.ServiceType.Equals(ServiceType.Link) && (a.ServiceOwnerCode.ToLower().Equals(org.ToLower()) || a.ServiceOwnerCode.ToLower().Equals("acn"))).ToList();
}
else
{
linkServices = unfiltered.Where(a => a.ServiceType.Equals(ServiceType.Link) && a.ServiceOwnerCode.ToLower().Equals(org.ToLower())).ToList();
}

_memoryCache.Set(cacheKey, linkServices, cacheEntryOptions);
}

Expand Down
1 change: 1 addition & 0 deletions backend/src/Designer/Designer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
<Watch Remove="Enums\ReferenceType.cs" />
<Watch Remove="Enums\ResourceType.cs" />
<Watch Remove="Enums\ServiceType.cs" />
<Watch Remove="Helpers\OrgUtil.cs" />
<Watch Remove="Helpers\ResourceAdminHelper.cs" />
<Watch Remove="Models\AvailableService.cs" />
<Watch Remove="Models\ConceptSchema.cs" />
Expand Down
12 changes: 12 additions & 0 deletions backend/src/Designer/Helpers/OrgUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace Altinn.Studio.Designer.Helpers
{
public static class OrgUtil
{
public static bool IsTestEnv(string org)
{
return string.Equals(org, "ttd", StringComparison.OrdinalIgnoreCase);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public async Task<List<AvailableService>> AvailableServices(int languageId, stri
{
List<AvailableService>? availableServices = null;

Check warning on line 56 in backend/src/Designer/TypedHttpClients/Altinn2Metadata/Altinn2MetadataClient.cs

View workflow job for this annotation

GitHub Actions / Run integration tests against actual gitea

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 56 in backend/src/Designer/TypedHttpClients/Altinn2Metadata/Altinn2MetadataClient.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (macos-latest)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 56 in backend/src/Designer/TypedHttpClients/Altinn2Metadata/Altinn2MetadataClient.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (windows-latest)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
string bridgeBaseUrl = GetSblBridgeUrl(environment);
string availabbleServicePath = $"h{bridgeBaseUrl}metadata/api/availableServices?languageID={languageId}&appTypesToInclude=0&includeExpired=false";
string availabbleServicePath = $"{bridgeBaseUrl}metadata/api/availableServices?languageID={languageId}&appTypesToInclude=0&includeExpired=false";

HttpResponseMessage response = await _httpClient.GetAsync(availabbleServicePath);

Expand All @@ -70,7 +70,7 @@ public async Task<List<AvailableService>> AvailableServices(int languageId, stri

private string GetSblBridgeUrl(string environment)
{
if (!_rrs.TryGetValue(environment, out ResourceRegistryEnvironmentSettings envSettings))
if (!_rrs.TryGetValue(environment.ToLower(), out ResourceRegistryEnvironmentSettings envSettings))
{
throw new ArgumentException($"Invalid environment. Missing environment config for {environment}");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public async Task ExportAltinn2Resource()
{
// Arrange
string uri = $"designer/api/ttd/resources/importresource/4485/4444/at23";
using (HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri))
using (HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, uri))
{
ServiceResource serviceResource = new ServiceResource()
{
Expand Down
3 changes: 2 additions & 1 deletion frontend/dashboard/pages/CreateService/CreateService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { SelectedContextType } from 'app-shared/navigation/main-header/Header';
import { useSelectedContext } from 'dashboard/hooks/useSelectedContext';
import { useNavigate } from 'react-router-dom';
import { AxiosError } from 'axios';
import { ServerCodes } from 'app-shared/enums/ServerCodes';

enum PageState {
Idle = 'Idle',
Expand Down Expand Up @@ -111,7 +112,7 @@ export const CreateService = ({ user, organizations }: CreateServiceProps): JSX.
);
},
onError: (error: { response: { status: number } }) => {
if (error.response.status === 409) {
if (error.response.status === ServerCodes.Conflict) {
setRepoErrorMessage(t('dashboard.app_already_exists'));
}

Expand Down
2 changes: 2 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,8 @@
"resourceadm.deploy_version_upload_button": "Last opp dine endringer",
"resourceadm.error_back_to_dashboard": "Gå tilbake til dashbord",
"resourceadm.error_page_text": "Du har nådd en ugyldig adresse",
"resourceadm.import_resource_empty_list": "Det finnes ingen servicer i {{env}}-miljøet",
"resourceadm.import_resource_spinner": "Importerer ressursen",
"resourceadm.left_nav_bar_about": "Om ressursen",
"resourceadm.left_nav_bar_back": "Tilbake til dashbord",
"resourceadm.left_nav_bar_back_icon": "Tilbake til dashbord",
Expand Down
2 changes: 2 additions & 0 deletions frontend/packages/shared/src/api/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
publishResourcePath,
appMetadataPath,
serviceConfigPath,
importResourceFromAltinn2Path,
} from 'app-shared/api/paths';
import { AddLanguagePayload } from 'app-shared/types/api/AddLanguagePayload';
import { AddRepoParams } from 'app-shared/types/api';
Expand Down Expand Up @@ -94,3 +95,4 @@ export const updatePolicy = (org: string, repo: string, id: string, payload: Pol
export const createResource = (org: string, payload: NewResource) => post(resourceCreatePath(org), payload);
export const updateResource = (org: string, repo: string, payload: Resource) => put(resourceEditPath(org, repo), payload);
export const publishResource = (org: string, repo: string, id: string, env: string) => post(publishResourcePath(org, repo, id, env), { headers: { 'Content-Type': 'application/json' } });
export const importResourceFromAltinn2 = (org: string, environment: string, serviceCode: string, serviceEdition: string) => post<Resource>(importResourceFromAltinn2Path(org, environment, serviceCode, serviceEdition));
2 changes: 2 additions & 0 deletions frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ export const resourceEditPath = (org, id) => `${basePath}/${org}/resources/updat
export const resourceValidatePolicyPath = (org, repo, id) => `${basePath}/${org}/${repo}/policy/validate/${id}`; // Get
export const resourceValidateResourcePath = (org, repo, id) => `${basePath}/${org}/resources/validate/${repo}/${id}`; // Get
export const publishResourcePath = (org, repo, id, env) => `${basePath}/${org}/resources/publish/${repo}/${id}?env=${env}`; // Get
export const altinn2LinkServicesPath = (org, env) => `${basePath}/${org}/resources/altinn2linkservices/${env}`; // Get
export const importResourceFromAltinn2Path = (org, env, serviceCode, serviceEdition) => `${basePath}/${org}/resources/importresource/${serviceCode}/${serviceEdition}/${env}`; // Post

// Process Editor
export const processEditorPath = (org, repo) => `${basePath}/${org}/${repo}/process-modelling/process-definition`;
3 changes: 3 additions & 0 deletions frontend/packages/shared/src/api/queries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { get, put } from 'app-shared/utils/networking';
import {
altinn2LinkServicesPath,
appMetadataPath,
appPolicyPath,
branchStatusPath,
Expand Down Expand Up @@ -64,6 +65,7 @@ import type { Resource, ResourceListItem, ResourceVersionStatus, Validation } fr
import type { AppConfig } from 'app-shared/types/AppConfig';
import type { Commit } from 'app-shared/types/Commit';
import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata';
import { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService';

export const getAppReleases = (owner: string, app: string) => get<AppReleasesResponse>(releasesPath(owner, app, 'Descending'));
export const getBranchStatus = (owner: string, app: string, branch: string) => get<BranchStatus>(branchStatusPath(owner, app, branch));
Expand Down Expand Up @@ -115,6 +117,7 @@ export const getResourceList = (org: string) => get<ResourceListItem[]>(resource
export const getResource = (org: string, repo: string, id: string) => get<Resource>(resourceSinglePath(org, repo, id));
export const getValidatePolicy = (org: string, repo: string, id: string) => get<Validation>(resourceValidatePolicyPath(org, repo, id));
export const getValidateResource = (org: string, repo: string, id: string) => get<Validation>(resourceValidateResourcePath(org, repo, id));
export const getAltinn2LinkServices = (org: string, environment: string) => get<Altinn2LinkService[]>(altinn2LinkServicesPath(org, environment));

// ProcessEditor
export const getBpmnFile = (org: string, app: string) => get(processEditorPath(org, app));
Expand Down
3 changes: 3 additions & 0 deletions frontend/packages/shared/src/enums/ServerCodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum ServerCodes {
Conflict = 409,
}
2 changes: 2 additions & 0 deletions frontend/packages/shared/src/mocks/queriesMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,6 @@ export const queriesMock: ServicesContextProps = {
updateAppConfig: jest.fn(),
getRepoInitialCommit: jest.fn(),
publishResource: jest.fn(),
getAltinn2LinkServices: jest.fn(),
importResourceFromAltinn2: jest.fn(),
};
5 changes: 5 additions & 0 deletions frontend/packages/shared/src/types/Altinn2LinkService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Altinn2LinkService {
serviceName: string;
externalServiceCode: string;
externalServiceEditionCode: string;
}
2 changes: 2 additions & 0 deletions frontend/packages/shared/src/types/QueryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@ export enum QueryKey {
ValidatePolicy = 'ValidatePolicy',
ValidateResource = 'ValidateResource',
PublishResource = 'PublishResource',
Altinn2Services = 'Altinn2Services',
ImportAltinn2Resource = 'ImportAltinn2Resource',
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@
margin-left: 10px;
}

.contentDivider {
border: solid 1px #d6d6d6;
margin-top: 25px;
margin-bottom: 15px;
width: 80%;
}

.contentWidth {
max-width: 600px;
}
Original file line number Diff line number Diff line change
@@ -1,45 +1,89 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render as rtlRender, screen, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ImportResourceModal, ImportResourceModalProps } from './ImportResourceModal'; // Update the import path
import { ImportResourceModal, ImportResourceModalProps } from './ImportResourceModal';
import { textMock } from '../../../testing/mocks/i18nMock';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import { ServicesContextProps, ServicesContextProvider } from 'app-shared/contexts/ServicesContext';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService';
import { useImportResourceFromAltinn2Mutation } from 'resourceadm/hooks/mutations';
import { UseMutationResult } from '@tanstack/react-query';
import { Resource } from 'app-shared/types/ResourceAdm';

describe('ImportResourceModal', () => {
const mockOnClose = jest.fn();
const mockAltinn2LinkService: Altinn2LinkService = {
externalServiceCode: 'code1',
externalServiceEditionCode: 'edition1',
serviceName: 'TestService',
};
const mockAltinn2LinkServices: Altinn2LinkService[] = [mockAltinn2LinkService];
const mockOption: string = `${mockAltinn2LinkService.externalServiceCode}-${mockAltinn2LinkService.externalServiceEditionCode}-${mockAltinn2LinkService.serviceName}`;

const defaultProps: ImportResourceModalProps = {
isOpen: true,
onClose: mockOnClose,
};
const mockOnClose = jest.fn();
const getAltinn2LinkServices = jest.fn().mockImplementation(() => Promise.resolve({}));

jest.mock('../../hooks/mutations/useImportResourceFromAltinn2Mutation');
const importResourceFromAltinn2 = jest.fn();
const mockImportResourceFromAltinn2 = useImportResourceFromAltinn2Mutation as jest.MockedFunction<
typeof useImportResourceFromAltinn2Mutation
>;
mockImportResourceFromAltinn2.mockReturnValue({
mutate: importResourceFromAltinn2,
} as unknown as UseMutationResult<
Resource,
unknown,
{
environment: string;
serviceCode: string;
serviceEdition: string;
},
unknown
>);

const defaultProps: ImportResourceModalProps = {
isOpen: true,
onClose: mockOnClose,
};

describe('ImportResourceModal', () => {
afterEach(jest.clearAllMocks);

it('selects environment and service, then checks if import button exists', async () => {
const user = userEvent.setup();

render(<ImportResourceModal {...defaultProps} />);
render();

const importButtonText = textMock('resourceadm.dashboard_import_modal_import_button');
const importButton = screen.queryByRole('button', { name: importButtonText });
expect(importButton).not.toBeInTheDocument();

const [, environmentSelect] = screen.getAllByLabelText(textMock('resourceadm.dashboard_import_modal_select_env'));
const [, environmentSelect] = screen.getAllByLabelText(
textMock('resourceadm.dashboard_import_modal_select_env'),
);
await act(() => user.click(environmentSelect));
await act(() => user.click(screen.getByRole('option', { name: 'AT21' })))
await act(() => user.click(screen.getByRole('option', { name: 'AT21' })));

expect(environmentSelect).toHaveValue('AT21');
expect(importButton).not.toBeInTheDocument();

const [, serviceSelect] = screen.getAllByLabelText(textMock('resourceadm.dashboard_import_modal_select_service'));
await waitForElementToBeRemoved(() =>

Check failure on line 70 in frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx

View workflow job for this annotation

GitHub Actions / Testing

ImportResourceModal › selects environment and service

The element(s) given to waitForElementToBeRemoved are already removed. waitForElementToBeRemoved requires that the element(s) exist(s) before waiting for removal. at initialCheck (../node_modules/@testing-library/react/node_modules/@testing-library/dom/dist/wait-for-element-to-be-removed.js:14:11) at waitForElementToBeRemoved (../node_modules/@testing-library/react/node_modules/@testing-library/dom/dist/wait-for-element-to-be-removed.js:31:3) at Object.<anonymous> (resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx:70:36)
screen.queryByTitle(textMock('resourceadm.import_resource_spinner')),
);

const [, serviceSelect] = screen.getAllByLabelText(
textMock('resourceadm.dashboard_import_modal_select_service'),
);
await act(() => user.click(serviceSelect));
await act(() => user.click(screen.getByRole('option', { name: 'Service1' })))
await act(() => user.click(screen.getByRole('option', { name: mockOption })));

expect(serviceSelect).toHaveValue('Service1');
expect(serviceSelect).toHaveValue(mockOption);
expect(screen.getByRole('button', { name: importButtonText })).toBeInTheDocument();
});

it('calls onClose function when close button is clicked', async () => {
const user = userEvent.setup();
render(<ImportResourceModal {...defaultProps} />);
render();

const closeButton = screen.getByRole('button', { name: textMock('general.cancel') });
await act(() => user.click(closeButton));
Expand All @@ -48,8 +92,52 @@ describe('ImportResourceModal', () => {
});

it('should be closed by default', () => {
render(<ImportResourceModal isOpen={false} onClose={() => {}} />);
render({ isOpen: false });

const closeButton = screen.queryByRole('button', { name: textMock('general.cancel') });
expect(closeButton).not.toBeInTheDocument();
});

it('calls import resource from Altinn 2 when import is clicked', async () => {
const user = userEvent.setup();
render();

const [, environmentSelect] = screen.getAllByLabelText(
textMock('resourceadm.dashboard_import_modal_select_env'),
);
await act(() => user.click(environmentSelect));
await act(() => user.click(screen.getByRole('option', { name: 'AT21' })));
await waitForElementToBeRemoved(() =>
screen.queryByTitle(textMock('resourceadm.import_resource_spinner')),
);
const [, serviceSelect] = screen.getAllByLabelText(
textMock('resourceadm.dashboard_import_modal_select_service'),
);
await act(() => user.click(serviceSelect));
await act(() => user.click(screen.getByRole('option', { name: mockOption })));

const importButton = screen.getByRole('button', {
name: textMock('resourceadm.dashboard_import_modal_import_button'),
});
await act(() => user.click(importButton));

expect(importResourceFromAltinn2).toHaveBeenCalledTimes(1);
});
});

const render = (props: Partial<ImportResourceModalProps> = {}) => {
getAltinn2LinkServices.mockImplementation(() => Promise.resolve(mockAltinn2LinkServices));

const allQueries: ServicesContextProps = {
...queriesMock,
getAltinn2LinkServices,
};

return rtlRender(
<MemoryRouter>
<ServicesContextProvider {...allQueries} client={createQueryClientMock()}>
<ImportResourceModal {...defaultProps} {...props} />
</ServicesContextProvider>
</MemoryRouter>,
);
};
Loading

0 comments on commit 48f9b25

Please sign in to comment.