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

feat(contact): display Altinn Servicedesk contact if user belongs to org #14371

Merged
merged 32 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3470c3a
Contact page
framitdavid Jan 7, 2025
87ef351
use the new endpoint
framitdavid Jan 7, 2025
e7c1c97
added few tests
framitdavid Jan 7, 2025
224eae1
use a DTO
framitdavid Jan 7, 2025
dbe7cdc
use a DTO
framitdavid Jan 7, 2025
4a79dab
merge main into feature
framitdavid Jan 7, 2025
ce6e47a
revert formatting
framitdavid Jan 7, 2025
900aa3f
format
framitdavid Jan 7, 2025
b9f3545
use isAuthenticated on the HttpContext instead
framitdavid Jan 7, 2025
bf999c0
added simple test for backend
framitdavid Jan 7, 2025
d96358d
PR feedback from code scanning
framitdavid Jan 7, 2025
130d63c
formatting
framitdavid Jan 7, 2025
90ca487
Update ContactPage.test.tsx PR feedback
framitdavid Jan 8, 2025
fa2d56c
Merge branch 'main' into feat/contactPage
framitdavid Jan 8, 2025
61fdb78
added method to queriesMock
framitdavid Jan 8, 2025
b3f66f6
Merge branch 'feat/contactPage' of https://github.com/Altinn/altinn-s…
framitdavid Jan 8, 2025
0d84b36
added a test for AllowAnonymous
framitdavid Jan 14, 2025
2c3f6c1
remove comment
framitdavid Jan 14, 2025
2502d01
Merge branch 'main' into feat/contactPage
framitdavid Jan 14, 2025
390c9b5
coderabbit PR feedback
framitdavid Jan 14, 2025
c8e09ec
Merge branch 'feat/contactPage' of https://github.com/Altinn/altinn-s…
framitdavid Jan 14, 2025
9e2eca4
PR feedback
framitdavid Jan 14, 2025
876ccd9
mocks for unit-test
framitdavid Jan 14, 2025
5bec87a
revert
framitdavid Jan 15, 2025
275967f
fixed tests
framitdavid Jan 15, 2025
85e2fb9
revert
framitdavid Jan 15, 2025
2382643
client id and clinet secret for tests
framitdavid Jan 15, 2025
ffdf3f8
Format
framitdavid Jan 15, 2025
8cf66d5
removed unsued usings
framitdavid Jan 15, 2025
8f1cef7
Merge branch 'main' into feat/contactPage
framitdavid Jan 15, 2025
a95d93f
tests
framitdavid Jan 16, 2025
ee72d88
Merge branch 'feat/contactPage' of https://github.com/Altinn/altinn-s…
framitdavid Jan 16, 2025
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
42 changes: 42 additions & 0 deletions backend/src/Designer/Controllers/ContactController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Altinn.Studio.Designer.Controllers
{
[Route("designer/api/[controller]")]
[ApiController]
public class ContactController : ControllerBase
{
private readonly IGitea _giteaService;

public ContactController(IGitea giteaService)
{
_giteaService = giteaService;
}
framitdavid marked this conversation as resolved.
Show resolved Hide resolved

[AllowAnonymous]
[HttpGet("belongs-to-org")]
public async Task<IActionResult> BelongsToOrg()
{
bool isNotAuthenticated = !AuthenticationHelper.IsAuthenticated(HttpContext);
if (isNotAuthenticated)
{
return Ok(new BelongsToOrgDto { BelongsToOrg = false });
}

try
{
var organizations = await _giteaService.GetUserOrganizations();
return Ok(new BelongsToOrgDto { BelongsToOrg = organizations.Count > 0 });
}
catch (Exception)
{
return Ok(new BelongsToOrgDto { BelongsToOrg = false });
}
framitdavid marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
5 changes: 5 additions & 0 deletions backend/src/Designer/Helpers/AuthenticationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ public static Task<string> GetDeveloperAppTokenAsync(this HttpContext context)
{
return context.GetTokenAsync("access_token");
}

public static bool IsAuthenticated(HttpContext context)
{
return context.User.Identity?.IsAuthenticated ?? false;
}
}
}
7 changes: 7 additions & 0 deletions backend/src/Designer/Models/Dto/BelongsToOrg.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Text.Json.Serialization;

public class BelongsToOrgDto
{
[JsonPropertyName("belongsToOrg")]
public bool BelongsToOrg { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Designer.Tests.Controllers.ApiTests;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace Designer.Tests.Controllers.ContactController;

public class FetchBelongsToOrgTests : DesignerEndpointsTestsBase<FetchBelongsToOrgTests>,
IClassFixture<WebApplicationFactory<Program>>
{
public FetchBelongsToOrgTests(WebApplicationFactory<Program> factory) : base(factory)
{
}

[Fact]
public async Task UsersThatBelongsToOrg_ShouldReturn_True()
{
string url = "/designer/api/contact/belongs-to-org";

using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);

var response = await HttpClient.SendAsync(httpRequestMessage);
var responseContent = await response.Content.ReadAsAsync<BelongsToOrgDto>();

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(responseContent.BelongsToOrg);
}

[Fact]
public async Task UsersThatDoNotBelongsToOrg_ShouldReturn_False_IfAnonymousUser()
{
string configPath = GetConfigPath();
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile(configPath, false, false)
.AddJsonStream(GenerateJsonOverrideConfig())
.AddEnvironmentVariables()
.Build();

var anonymousClient = Factory.WithWebHostBuilder(builder =>
{
builder.UseConfiguration(configuration);
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Anonymous")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Anonymous", options => { });
});
}).CreateDefaultClient();

string url = "/designer/api/contact/belongs-to-org";

using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);

var response = await anonymousClient.SendAsync(httpRequestMessage);
var responseContent = await response.Content.ReadAsAsync<BelongsToOrgDto>();

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.False(responseContent.BelongsToOrg);
}
}
9 changes: 8 additions & 1 deletion backend/tests/Designer.Tests/Mocks/IGiteaMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Altinn.Studio.Designer.Services.Interfaces;

using Designer.Tests.Utils;
using Organization = Altinn.Studio.Designer.RepositoryClient.Model.Organization;

namespace Designer.Tests.Mocks
{
Expand Down Expand Up @@ -131,7 +132,13 @@ public Task<List<Team>> GetTeams()

public Task<List<Organization>> GetUserOrganizations()
{
throw new NotImplementedException();
var organizations = new List<Organization>
{
new Organization { Username = "Org1", Id = 1 }, // Example items
new Organization { Username = "Org2", Id = 2 }
};

return Task.FromResult(organizations);
}

public Task<IList<Repository>> GetUserRepos()
Expand Down
5 changes: 5 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,16 @@
"code_list_editor.text_resource.label.select": "Finn ledetekst for verdi nummer {{number}}",
"code_list_editor.text_resource.label.value": "Oppgi ledetekst for verdi nummer {{number}}",
"code_list_editor.value_item": "Verdi for alternativ {{number}}",
"contact.altinn_servicedesk.content": "Er du tjenesteeier og har du behov for hjelp? Ta kontakt med oss!",
"contact.altinn_servicedesk.heading": "Altinn Servicedesk",
"contact.email.content": "Du kan skrive en e-post til Altinn servicedesk hvis du har spørsmål om å opprette organisasjoner eller miljøer, opplever tekniske problemer eller har spørsmål om dokumentasjonen eller andre ting.",
"contact.email.heading": "Send e-post",
"contact.github_issue.content": "Hvis du har behov for funksjonalitet eller ser feil og mangler i Studio som vi må fikse, kan du opprette en sak i Github, så ser vi på den.",
"contact.github_issue.heading": "Rapporter feil og mangler til oss",
"contact.github_issue.link_label": "Opprett sak i Github",
"contact.serviceDesk.email": "<b>E-post:</b> <a>[email protected]</a>",
"contact.serviceDesk.emergencyPhone": "<b>Vakttelefon:</b> <a>94 49 00 02</a> (tilgjengelig kl. 15:45–07:00)",
"contact.serviceDesk.phone": "<b>Telefon:</b> <a>75 00 62 99</a>",
"contact.slack.content": "Hvis du har spørsmål om hvordan du bygger en app, kan du snakke direkte med utviklingsteamet i Altinn Studio på Slack. De hjelper deg med å",
"contact.slack.content_list": "<0>bygge appene slik du ønsker</0><0>svare på spørsmål og veilede deg</0><0>ta imot innspill på ny funksjonalitet</0>",
"contact.slack.heading": "Skriv melding til oss Slack",
Expand Down
3 changes: 3 additions & 0 deletions frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,6 @@ export const processEditorDataTypePath = (org, app, dataTypeId, taskId) => `${ba

// Event Hubs
export const SyncEventsWebSocketHub = () => '/sync-hub';

// Contact
export const belongsToOrg = () => `${basePath}/contact/belongs-to-org`;
4 changes: 4 additions & 0 deletions frontend/packages/shared/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
appMetadataPath,
appPolicyPath,
appVersionPath,
belongsToOrg,
branchStatusPath,
dataModelMetadataPath,
dataModelPath,
Expand Down Expand Up @@ -168,3 +169,6 @@ export const getAltinn2DelegationsCount = (org: string, serviceCode: string, ser
// ProcessEditor
export const getBpmnFile = (org: string, app: string) => get<string>(processEditorPath(org, app));
export const getProcessTaskType = (org: string, app: string, taskId: string) => get<string>(`${processTaskTypePath(org, app, taskId)}`);

// Contact Page
export const fetchBelongsToGiteaOrg = () => get(belongsToOrg());
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type GetInTouchProvider } from '../interfaces/GetInTouchProvider';

type PhoneChannel = 'phone' | 'emergencyPhone';

const phoneChannelMap: Record<PhoneChannel, string> = {
phone: 'tel:75006299',
emergencyPhone: 'tel:94490002',
};

export class PhoneContactProvider implements GetInTouchProvider<PhoneChannel> {
public buildContactUrl(selectedChannel: PhoneChannel): string {
return phoneChannelMap[selectedChannel];
}
}
5 changes: 5 additions & 0 deletions frontend/packages/shared/src/mocks/queriesMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ export const queriesMock: ServicesContextProps = {
.mockImplementation(() => Promise.resolve<MaskinportenScope[]>([])),
updateSelectedMaskinportenScopes: jest.fn().mockImplementation(() => Promise.resolve()),

// Queries - Contact
fetchBelongsToGiteaOrg: jest
.fn()
.mockImplementation(() => Promise.resolve({ belongsToOrg: true })),

// Mutations
addAppAttachmentMetadata: jest.fn().mockImplementation(() => Promise.resolve()),
addDataTypeToAppMetadata: jest.fn().mockImplementation(() => Promise.resolve()),
Expand Down
7 changes: 1 addition & 6 deletions frontend/packages/shared/src/types/QueryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum QueryKey {
AppPolicy = 'AppPolicy',
AppReleases = 'AppReleases',
AppVersion = 'AppVersion',
BelongsToOrg = 'BelongsToOrg',
BranchStatus = 'BranchStatus',
CurrentUser = 'CurrentUser',
DataModelMetadata = 'DataModelMetadata',
Expand All @@ -14,7 +15,6 @@ export enum QueryKey {
DeployPermissions = 'DeployPermissions',
Environments = 'Environments',
FetchBpmn = 'FetchBpmn',
FetchTextResources = 'FetchTextResources',
FormComponent = 'FormComponent',
FormLayoutSettings = 'FormLayoutSettings',
FormLayouts = 'FormLayouts',
Expand All @@ -24,7 +24,6 @@ export enum QueryKey {
InstanceId = 'InstanceId',
JsonSchema = 'JsonSchema',
LayoutNames = 'LayoutNames',
LayoutSchema = 'LayoutSchema',
LayoutSets = 'LayoutSets',
LayoutSetsExtended = 'LayoutSetsExtended',
OptionList = 'OptionList',
Expand All @@ -36,7 +35,6 @@ export enum QueryKey {
ProcessTaskDataType = 'ProcessTaskDataType',
RepoMetadata = 'RepoMetadata',
RepoPullData = 'RepoPullData',
RepoReset = 'RepoReset',
RepoStatus = 'RepoStatus',
RepoDiff = 'RepoDiff',
RuleConfig = 'RuleConfig',
Expand All @@ -60,9 +58,6 @@ export enum QueryKey {
ResourcePolicyAccessPackages = 'ResourcePolicyAccessPackages',
ResourcePolicyAccessPackageServices = 'ResourcePolicyAccessPackageServices',
ResourcePublishStatus = 'ResourcePublishStatus',
ResourceSectors = 'ResourceSectors',
ResourceThematicEurovoc = 'ResourceThematicEurovoc',
ResourceThematicLos = 'ResourceThematicLos',
SingleResource = 'SingleResource',
ValidatePolicy = 'ValidatePolicy',
ValidateResource = 'ValidateResource',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import classes from './ContactSection.module.css';
export type ContactSectionProps = {
title: string;
description: string;
link: {
link?: {
name: string;
href: string;
};
Expand All @@ -31,7 +31,7 @@ export const ContactSection = ({
</StudioHeading>
<StudioParagraph spacing>{description}</StudioParagraph>
{additionalContent && <span>{additionalContent}</span>}
<StudioLink href={link.href}>{link.name}</StudioLink>
{link && <StudioLink href={link.href}>{link.name}</StudioLink>}
</div>
</section>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { type ReactElement } from 'react';
import { GetInTouchWith } from 'app-shared/getInTouch';
import { EmailContactProvider } from 'app-shared/getInTouch/providers';
import { StudioList, StudioLink } from '@studio/components';
import { Trans } from 'react-i18next';
import { PhoneContactProvider } from 'app-shared/getInTouch/providers/PhoneContactProvider';

export const ContactServiceDesk = (): ReactElement => {
const contactByEmail = new GetInTouchWith(new EmailContactProvider());
const contactByPhone = new GetInTouchWith(new PhoneContactProvider());
return (
<StudioList.Root>
<StudioList.Unordered>
<StudioList.Item>
<Trans
i18nKey='contact.serviceDesk.phone'
components={{
b: <b />,
a: <StudioLink href={contactByPhone.url('phone')}>{null}</StudioLink>,
}}
/>
</StudioList.Item>

<StudioList.Item>
<Trans
i18nKey='contact.serviceDesk.emergencyPhone'
values={{ phoneNumber: contactByPhone.url('phone') }}
components={{
b: <b />,
a: <StudioLink href={contactByPhone.url('emergencyPhone')}>{null}</StudioLink>,
}}
/>
</StudioList.Item>

<StudioList.Item>
<Trans
i18nKey='contact.serviceDesk.email'
values={{ phoneNumber: contactByPhone.url('phone') }}
components={{
b: <b />,
a: <StudioLink href={contactByEmail.url('serviceOwner')}>{null}</StudioLink>,
}}
/>
</StudioList.Item>
</StudioList.Unordered>
</StudioList.Root>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ContactServiceDesk } from './ContactServiceDesk';
32 changes: 32 additions & 0 deletions frontend/studio-root/pages/Contact/ContactPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import React from 'react';
import { screen, render } from '@testing-library/react';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { ContactPage } from './ContactPage';
import { useFetchBelongsToOrgQuery } from '../hooks/queries/useFetchBelongsToOrgQuery';

jest.mock('../hooks/queries/useFetchBelongsToOrgQuery');

(useFetchBelongsToOrgQuery as jest.Mock).mockReturnValue({
data: { belongsToOrg: false },
});

describe('ContactPage', () => {
it('should display the main heading', () => {
Expand Down Expand Up @@ -44,4 +51,29 @@ describe('ContactPage', () => {
screen.getByRole('link', { name: textMock('contact.github_issue.link_label') }),
).toBeInTheDocument();
});

it('should not render contact info for "Altinn Servicedesk" if the user does not belong to a org', () => {
(useFetchBelongsToOrgQuery as jest.Mock).mockReturnValue({
data: { belongsToOrg: false },
});
render(<ContactPage />);

expect(
screen.queryByRole('heading', { name: textMock('contact.altinn_servicedesk.heading') }),
).not.toBeInTheDocument();
expect(
screen.queryByText(textMock('contact.altinn_servicedesk.content')),
).not.toBeInTheDocument();
});

it('should display contact information to "Altinn Servicedesk" if user belongs to an org', () => {
(useFetchBelongsToOrgQuery as jest.Mock).mockReturnValue({
data: { belongsToOrg: true },
});
render(<ContactPage />);
expect(
screen.getByRole('heading', { name: textMock('contact.altinn_servicedesk.heading') }),
).toBeInTheDocument();
expect(screen.getByText(textMock('contact.altinn_servicedesk.content'))).toBeInTheDocument();
});
});
Loading
Loading