From 3470c3a3dedf7322e384603d1d52b72433aef379 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 7 Jan 2025 13:41:23 +0100 Subject: [PATCH 01/13] Contact page --- .../Designer/Controllers/ContactController.cs | 41 ++++++++++++ .../src/Designer/Models/Dto/BelongsToOrg.cs | 4 ++ backend/src/Designer/Program.cs | 64 +++++++++++-------- .../ContactController/GetBelongsToOrgTests.cs | 35 ++++++++++ frontend/language/src/nb.json | 5 ++ .../providers/PhoneContactProvider.ts | 14 ++++ .../ContactSection/ContactSection.tsx | 4 +- .../ContactServiceDesk/ContactServiceDesk.tsx | 48 ++++++++++++++ .../components/ContactServiceDesk/index.ts | 1 + .../pages/Contact/ContactPage.test.tsx | 6 ++ .../studio-root/pages/Contact/ContactPage.tsx | 9 ++- 11 files changed, 201 insertions(+), 30 deletions(-) create mode 100644 backend/src/Designer/Controllers/ContactController.cs create mode 100644 backend/src/Designer/Models/Dto/BelongsToOrg.cs create mode 100644 backend/tests/Designer.Tests/Controllers/ContactController/GetBelongsToOrgTests.cs create mode 100644 frontend/packages/shared/src/getInTouch/providers/PhoneContactProvider.ts create mode 100644 frontend/studio-root/components/ContactServiceDesk/ContactServiceDesk.tsx create mode 100644 frontend/studio-root/components/ContactServiceDesk/index.ts diff --git a/backend/src/Designer/Controllers/ContactController.cs b/backend/src/Designer/Controllers/ContactController.cs new file mode 100644 index 00000000000..f1d2650085c --- /dev/null +++ b/backend/src/Designer/Controllers/ContactController.cs @@ -0,0 +1,41 @@ +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; + } + + [AllowAnonymous] + [HttpGet("contact/belongs-to-org")] + public async Task BelongsToOrg() + { + bool isNotAuthenticated = string.IsNullOrEmpty(AuthenticationHelper.GetDeveloperUserName(HttpContext)); + if (isNotAuthenticated) + { + return Ok(new { belongsToOrg = false }); + } + + try + { + var organizations = await _giteaService.GetUserOrganizations(); + return Ok(new { belongsToOrg = organizations.Count > 0 }); + } + catch + { + return Ok(new { belongsToOrg = false }); + } + } +} diff --git a/backend/src/Designer/Models/Dto/BelongsToOrg.cs b/backend/src/Designer/Models/Dto/BelongsToOrg.cs new file mode 100644 index 00000000000..2b6038d1a05 --- /dev/null +++ b/backend/src/Designer/Models/Dto/BelongsToOrg.cs @@ -0,0 +1,4 @@ +public class BelongsToOrg +{ + public bool belongsToOrg { get; set; } +} diff --git a/backend/src/Designer/Program.cs b/backend/src/Designer/Program.cs index bae7c0a7425..c1bd0682ba8 100644 --- a/backend/src/Designer/Program.cs +++ b/backend/src/Designer/Program.cs @@ -77,7 +77,8 @@ async Task SetConfigurationProviders(ConfigurationManager config, IWebHostEnviro logger.LogInformation("// Program.cs // SetConfigurationProviders // Attempting to configure providers"); string basePath = Directory.GetParent(Directory.GetCurrentDirectory()).FullName; config.SetBasePath(basePath); - config.AddJsonFile(basePath + "app/altinn-appsettings/altinn-appsettings-secret.json", optional: true, reloadOnChange: true); + config.AddJsonFile(basePath + "app/altinn-appsettings/altinn-appsettings-secret.json", optional: true, + reloadOnChange: true); string envName = hostingEnvironment.EnvironmentName; if (basePath == "/") @@ -86,7 +87,8 @@ async Task SetConfigurationProviders(ConfigurationManager config, IWebHostEnviro } else { - config.AddJsonFile(Directory.GetCurrentDirectory() + "/appsettings.json", optional: false, reloadOnChange: true); + config.AddJsonFile(Directory.GetCurrentDirectory() + "/appsettings.json", optional: false, + reloadOnChange: true); } config.AddEnvironmentVariables(); @@ -101,7 +103,9 @@ async Task SetConfigurationProviders(ConfigurationManager config, IWebHostEnviro !string.IsNullOrEmpty(keyVaultSettings.SecretUri)) { logger.LogInformation("// Program.cs // SetConfigurationProviders // Attempting to configure KeyVault"); - AzureServiceTokenProvider azureServiceTokenProvider = new($"RunAs=App;AppId={keyVaultSettings.ClientId};TenantId={keyVaultSettings.TenantId};AppKey={keyVaultSettings.ClientSecret}"); + AzureServiceTokenProvider azureServiceTokenProvider = + new( + $"RunAs=App;AppId={keyVaultSettings.ClientId};TenantId={keyVaultSettings.TenantId};AppKey={keyVaultSettings.ClientSecret}"); KeyVaultClient keyVaultClient = new( new KeyVaultClient.AuthenticationCallback( azureServiceTokenProvider.KeyVaultTokenCallback)); @@ -123,7 +127,8 @@ async Task SetConfigurationProviders(ConfigurationManager config, IWebHostEnviro if (hostingEnvironment.IsDevelopment() && !Directory.GetCurrentDirectory().Contains("app")) { - config.AddJsonFile(Directory.GetCurrentDirectory() + $"/appsettings.{envName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile(Directory.GetCurrentDirectory() + $"/appsettings.{envName}.json", optional: true, + reloadOnChange: true); Assembly assembly = Assembly.Load(new AssemblyName(hostingEnvironment.ApplicationName)); if (assembly != null) { @@ -151,20 +156,23 @@ void ConfigureLogging(ILoggingBuilder builder) // standalone package Microsoft.Extensions.Logging.ApplicationInsights // or if you want to capture logs from early in the application startup // pipeline from Startup.cs or Program.cs itself. - builder.AddApplicationInsights(configureTelemetryConfiguration: config => - { - config.ConnectionString = applicationInsightsConnectionString; - }, - configureApplicationInsightsLoggerOptions: _ => { }); + builder.AddApplicationInsights( + configureTelemetryConfiguration: config => + { + config.ConnectionString = applicationInsightsConnectionString; + }, + configureApplicationInsightsLoggerOptions: _ => { }); // Optional: Apply filters to control what logs are sent to Application Insights. // The following configures LogLevel Information or above to be sent to // Application Insights for all categories. - builder.AddFilter(string.Empty, LogLevel.Warning); + builder.AddFilter( + string.Empty, LogLevel.Warning); // Adding the filter below to ensure logs of all severity from Program.cs // is sent to ApplicationInsights. - builder.AddFilter(typeof(Program).FullName, LogLevel.Trace); + builder.AddFilter( + typeof(Program).FullName, LogLevel.Trace); } else { @@ -179,17 +187,16 @@ void ConfigureServices(IServiceCollection services, IConfiguration configuration { logger.LogInformation("// Program.cs // ConfigureServices // Attempting to configure services"); - services.Configure(options => - { - options.AllowSynchronousIO = true; - }); + services.Configure(options => { options.AllowSynchronousIO = true; }); - services.ConfigureResourceRegistryIntegrationSettings(configuration.GetSection("ResourceRegistryIntegrationSettings")); + services.ConfigureResourceRegistryIntegrationSettings( + configuration.GetSection("ResourceRegistryIntegrationSettings")); services.ConfigureMaskinportenIntegrationSettings(configuration.GetSection("MaskinportenClientSettings")); services.Configure(configuration.GetSection("MaskinportenClientSettings")); var maskinPortenClientName = "MaskinportenClient"; - services.RegisterMaskinportenClientDefinition(maskinPortenClientName, configuration.GetSection("MaskinportenClientSettings")); + services.RegisterMaskinportenClientDefinition(maskinPortenClientName, + configuration.GetSection("MaskinportenClientSettings")); services.AddHttpClient(); var maskinportenSettings = new MaskinportenClientSettings(); @@ -219,14 +226,18 @@ void ConfigureServices(IServiceCollection services, IConfiguration configuration // Add application insight telemetry if (!string.IsNullOrEmpty(applicationInsightsConnectionString)) { - services.AddApplicationInsightsTelemetry(options => { options.ConnectionString = applicationInsightsConnectionString; }); + services.AddApplicationInsightsTelemetry(options => + { + options.ConnectionString = applicationInsightsConnectionString; + }); services.ConfigureTelemetryModule( (module, o) => { module.Counters.Clear(); module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "threadpool-queue-length")); module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "threadpool-thread-count")); - module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "monitor-lock-contention-count")); + module.Counters.Add( + new EventCounterCollectionRequest("System.Runtime", "monitor-lock-contention-count")); module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "gc-heap-size")); module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "time-in-gc")); module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "working-set")); @@ -296,11 +307,7 @@ void Configure(IConfiguration configuration) OnPrepareResponse = context => { ResponseHeaders headers = context.Context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue - { - Public = true, - MaxAge = TimeSpan.FromMinutes(60), - }; + headers.CacheControl = new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromMinutes(60), }; } }); @@ -337,10 +344,11 @@ void CreateDirectory(IConfiguration configuration) // TODO: Figure out how appsettings.json parses values and merges with environment variables and use these here. // Since ":" is not valid in environment variables names in kubernetes, we can't use current docker-compose environment variables var repoLocation = Environment.GetEnvironmentVariable("ServiceRepositorySettings:RepositoryLocation") ?? - configuration["ServiceRepositorySettings:RepositoryLocation"]; + configuration["ServiceRepositorySettings:RepositoryLocation"]; if (string.IsNullOrWhiteSpace(repoLocation)) { - repoLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "altinn", "repos"); + repoLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "altinn", + "repos"); configuration.GetSection("ServiceRepositorySettings")["RepositoryLocation"] = repoLocation; } @@ -360,4 +368,6 @@ static string GetXmlCommentsPathForControllers() return xmlPath; } -public partial class Program { } +public partial class Program +{ +} diff --git a/backend/tests/Designer.Tests/Controllers/ContactController/GetBelongsToOrgTests.cs b/backend/tests/Designer.Tests/Controllers/ContactController/GetBelongsToOrgTests.cs new file mode 100644 index 00000000000..d98373a9e20 --- /dev/null +++ b/backend/tests/Designer.Tests/Controllers/ContactController/GetBelongsToOrgTests.cs @@ -0,0 +1,35 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Altinn.Studio.Designer.Configuration; +using Designer.Tests.Controllers.ApiTests; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace Designer.Tests.Controllers.ContactController; + +public class GetBelongsToOrgTests : DesignerEndpointsTestsBase, + IClassFixture> +{ + private const string Org = $"/designer/api/belongs-to-org"; + private static string VersionPrefix(string org, string repository) => $"/designer/api/{org}/{repository}/config"; + + public GetBelongsToOrgTests(WebApplicationFactory factory) : base(factory) + { + } + + [Theory] + public async Task GetBelongsToOrg() + { + string dataPathWithData = VersionPrefix(org, app); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, dataPathWithData); + + using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage); + response.EnsureSuccessStatusCode(); + ServiceConfiguration serviceConfigResponse = await response.Content.ReadAsAsync(); + ServiceConfiguration serviceConfiguration = new ServiceConfiguration { RepositoryName = app, ServiceDescription = null, ServiceId = null, ServiceName = null }; + + Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode); + Assert.Equal(serviceConfiguration.RepositoryName, serviceConfigResponse.RepositoryName); + } + +} diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index d6357c06149..fdd99709fa6 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -128,11 +128,16 @@ "code_list_editor.label_item": "Ledetekst for alternativ {{number}}", "code_list_editor.legend": "Kodeliste", "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": "E-post: tjenesteeier@altinn.no", + "contact.serviceDesk.emergencyPhone": "Vakttelefon: 94 49 00 02 (tilgjengelig kl. 15:45–07:00)", + "contact.serviceDesk.phone": "Telefon: 75 00 62 99", "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>svare på spørsmål og veilede deg<0>ta imot innspill på ny funksjonalitet", "contact.slack.heading": "Skriv melding til oss Slack", diff --git a/frontend/packages/shared/src/getInTouch/providers/PhoneContactProvider.ts b/frontend/packages/shared/src/getInTouch/providers/PhoneContactProvider.ts new file mode 100644 index 00000000000..d1c440c711e --- /dev/null +++ b/frontend/packages/shared/src/getInTouch/providers/PhoneContactProvider.ts @@ -0,0 +1,14 @@ +import { type GetInTouchProvider } from '../interfaces/GetInTouchProvider'; + +type PhoneChannel = 'phone' | 'emergencyPhone'; + +const phoneChannelMap: Record = { + phone: 'tel:75006299', + emergencyPhone: 'tel:94490002', +}; + +export class PhoneContactProvider implements GetInTouchProvider { + public buildContactUrl(selectedChannel: PhoneChannel): string { + return phoneChannelMap[selectedChannel]; + } +} diff --git a/frontend/studio-root/components/ContactSection/ContactSection.tsx b/frontend/studio-root/components/ContactSection/ContactSection.tsx index 64b1d882402..3d63de43ae5 100644 --- a/frontend/studio-root/components/ContactSection/ContactSection.tsx +++ b/frontend/studio-root/components/ContactSection/ContactSection.tsx @@ -6,7 +6,7 @@ import classes from './ContactSection.module.css'; export type ContactSectionProps = { title: string; description: string; - link: { + link?: { name: string; href: string; }; @@ -31,7 +31,7 @@ export const ContactSection = ({ {description} {additionalContent && {additionalContent}} - {link.name} + {link && {link.name}} ); diff --git a/frontend/studio-root/components/ContactServiceDesk/ContactServiceDesk.tsx b/frontend/studio-root/components/ContactServiceDesk/ContactServiceDesk.tsx new file mode 100644 index 00000000000..1a20ec1c2dd --- /dev/null +++ b/frontend/studio-root/components/ContactServiceDesk/ContactServiceDesk.tsx @@ -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 ( + + + + , + a: {null}, + }} + /> + + + + , + a: {null}, + }} + /> + + + + , + a: {null}, + }} + /> + + + + ); +}; diff --git a/frontend/studio-root/components/ContactServiceDesk/index.ts b/frontend/studio-root/components/ContactServiceDesk/index.ts new file mode 100644 index 00000000000..2cbb79dacd0 --- /dev/null +++ b/frontend/studio-root/components/ContactServiceDesk/index.ts @@ -0,0 +1 @@ +export { ContactServiceDesk } from './ContactServiceDesk'; diff --git a/frontend/studio-root/pages/Contact/ContactPage.test.tsx b/frontend/studio-root/pages/Contact/ContactPage.test.tsx index 7b4be059c57..09e96ed90af 100644 --- a/frontend/studio-root/pages/Contact/ContactPage.test.tsx +++ b/frontend/studio-root/pages/Contact/ContactPage.test.tsx @@ -33,6 +33,12 @@ describe('ContactPage', () => { expect(screen.getByRole('link', { name: textMock('contact.slack.link') })).toBeInTheDocument(); }); + it('should display contact information to "Altinn Servicedesk"', () => { + render(); + expect(screen.getByRole('heading', { name: textMock('contact.altinn_servicedesk.heading') })); + expect(screen.getByText(textMock('contact.altinn_servicedesk.content'))); + }); + it('should display the bug report and feature request section with its content and link', () => { render(); diff --git a/frontend/studio-root/pages/Contact/ContactPage.tsx b/frontend/studio-root/pages/Contact/ContactPage.tsx index 7aa6d6e8f8d..62680b3b3b1 100644 --- a/frontend/studio-root/pages/Contact/ContactPage.tsx +++ b/frontend/studio-root/pages/Contact/ContactPage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import classes from './ContactPage.module.css'; import { Trans, useTranslation } from 'react-i18next'; -import { EnvelopeClosedIcon, SlackIcon, GitHubIcon } from '@studio/icons'; +import { EnvelopeClosedIcon, SlackIcon, GitHubIcon, PersonHeadsetIcon } from '@studio/icons'; import { GetInTouchWith } from 'app-shared/getInTouch'; import { EmailContactProvider, @@ -14,6 +14,7 @@ import { StudioParagraph, } from '@studio/components'; import { ContactSection, type ContactSectionProps } from '../../components/ContactSection'; +import { ContactServiceDesk } from '../../components/ContactServiceDesk'; export const ContactPage = (): React.ReactElement => { const { t } = useTranslation(); @@ -58,6 +59,12 @@ export const ContactPage = (): React.ReactElement => { }, Icon: GitHubIcon, }, + { + title: t('contact.altinn_servicedesk.heading'), + additionalContent: , + description: t('contact.altinn_servicedesk.content'), + Icon: PersonHeadsetIcon, + }, ]; return ( From 87ef3514ca5429bf3d95854648457643a73ea606 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 7 Jan 2025 14:12:46 +0100 Subject: [PATCH 02/13] use the new endpoint --- .../Designer/Controllers/ContactController.cs | 2 +- frontend/packages/shared/src/api/paths.js | 3 +++ frontend/packages/shared/src/api/queries.ts | 4 ++++ frontend/packages/shared/src/types/QueryKey.ts | 7 +------ .../studio-root/pages/Contact/ContactPage.tsx | 16 ++++++++++++++-- .../hooks/queries/useFetchBelongsToOrgQuery.ts | 12 ++++++++++++ 6 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 frontend/studio-root/pages/hooks/queries/useFetchBelongsToOrgQuery.ts diff --git a/backend/src/Designer/Controllers/ContactController.cs b/backend/src/Designer/Controllers/ContactController.cs index f1d2650085c..db1331339e8 100644 --- a/backend/src/Designer/Controllers/ContactController.cs +++ b/backend/src/Designer/Controllers/ContactController.cs @@ -19,7 +19,7 @@ public ContactController(IGitea giteaService) } [AllowAnonymous] - [HttpGet("contact/belongs-to-org")] + [HttpGet("belongs-to-org")] public async Task BelongsToOrg() { bool isNotAuthenticated = string.IsNullOrEmpty(AuthenticationHelper.GetDeveloperUserName(HttpContext)); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index e749e05eaa1..7d71b5ef852 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -177,3 +177,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`; diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts index 57002072126..d4d3df58476 100644 --- a/frontend/packages/shared/src/api/queries.ts +++ b/frontend/packages/shared/src/api/queries.ts @@ -56,6 +56,7 @@ import { authStatusAnsattporten, availableMaskinportenScopesPath, selectedMaskinportenScopesPath, + belongsToOrg, } from './paths'; import type { AppReleasesResponse, DataModelMetadataResponse, SearchRepoFilterParams, SearchRepositoryResponse } from 'app-shared/types/api'; @@ -158,3 +159,6 @@ export const getAltinn2DelegationsCount = (org: string, serviceCode: string, ser // ProcessEditor export const getBpmnFile = (org: string, app: string) => get(processEditorPath(org, app)); export const getProcessTaskType = (org: string, app: string, taskId: string) => get(`${processTaskTypePath(org, app, taskId)}`); + +// Contact Page +export const fetchBelongsToGiteaOrg = () => get(belongsToOrg()); diff --git a/frontend/packages/shared/src/types/QueryKey.ts b/frontend/packages/shared/src/types/QueryKey.ts index 5195acbf358..3bf82fe68d4 100644 --- a/frontend/packages/shared/src/types/QueryKey.ts +++ b/frontend/packages/shared/src/types/QueryKey.ts @@ -5,6 +5,7 @@ export enum QueryKey { AppPolicy = 'AppPolicy', AppReleases = 'AppReleases', AppVersion = 'AppVersion', + BelongsToOrg = 'BelongsToOrg', BranchStatus = 'BranchStatus', CurrentUser = 'CurrentUser', DataModelMetadata = 'DataModelMetadata', @@ -14,7 +15,6 @@ export enum QueryKey { DeployPermissions = 'DeployPermissions', Environments = 'Environments', FetchBpmn = 'FetchBpmn', - FetchTextResources = 'FetchTextResources', FormComponent = 'FormComponent', FormLayoutSettings = 'FormLayoutSettings', FormLayouts = 'FormLayouts', @@ -24,7 +24,6 @@ export enum QueryKey { InstanceId = 'InstanceId', JsonSchema = 'JsonSchema', LayoutNames = 'LayoutNames', - LayoutSchema = 'LayoutSchema', LayoutSets = 'LayoutSets', LayoutSetsExtended = 'LayoutSetsExtended', OptionLists = 'OptionLists', @@ -34,7 +33,6 @@ export enum QueryKey { ProcessTaskDataType = 'ProcessTaskDataType', RepoMetadata = 'RepoMetadata', RepoPullData = 'RepoPullData', - RepoReset = 'RepoReset', RepoStatus = 'RepoStatus', RepoDiff = 'RepoDiff', RuleConfig = 'RuleConfig', @@ -55,9 +53,6 @@ export enum QueryKey { ResourcePolicyActions = 'ResourcePolicyActions', ResourcePolicySubjects = 'ResourcePolicySubjects', ResourcePublishStatus = 'ResourcePublishStatus', - ResourceSectors = 'ResourceSectors', - ResourceThematicEurovoc = 'ResourceThematicEurovoc', - ResourceThematicLos = 'ResourceThematicLos', SingleResource = 'SingleResource', ValidatePolicy = 'ValidatePolicy', ValidateResource = 'ValidateResource', diff --git a/frontend/studio-root/pages/Contact/ContactPage.tsx b/frontend/studio-root/pages/Contact/ContactPage.tsx index 62680b3b3b1..406f4fd69f3 100644 --- a/frontend/studio-root/pages/Contact/ContactPage.tsx +++ b/frontend/studio-root/pages/Contact/ContactPage.tsx @@ -15,6 +15,11 @@ import { } from '@studio/components'; import { ContactSection, type ContactSectionProps } from '../../components/ContactSection'; import { ContactServiceDesk } from '../../components/ContactServiceDesk'; +import { useFetchBelongsToOrgQuery } from '../hooks/queries/useFetchBelongsToOrgQuery'; + +type ContactSectionMetadata = { + shouldHideSection?: boolean; +}; export const ContactPage = (): React.ReactElement => { const { t } = useTranslation(); @@ -22,7 +27,9 @@ export const ContactPage = (): React.ReactElement => { const contactBySlack = new GetInTouchWith(new SlackContactProvider()); const contactByGitHubIssue = new GetInTouchWith(new GitHubIssueContactProvider()); - const contactSections: Array = [ + const { data: belongsToOrgData } = useFetchBelongsToOrgQuery(); + + const contactSections: Array = [ { title: t('contact.email.heading'), description: t('contact.email.content'), @@ -64,6 +71,7 @@ export const ContactPage = (): React.ReactElement => { additionalContent: , description: t('contact.altinn_servicedesk.content'), Icon: PersonHeadsetIcon, + shouldHideSection: !belongsToOrgData?.belongsToOrg, }, ]; @@ -76,7 +84,7 @@ export const ContactPage = (): React.ReactElement => { {t('general.contact')} - {contactSections.map((contactSection) => ( + {contactSections.filter(filterHiddenSections).map((contactSection) => ( ))} @@ -84,3 +92,7 @@ export const ContactPage = (): React.ReactElement => { ); }; + +function filterHiddenSections(section: ContactSectionProps & ContactSectionMetadata): boolean { + return !section.shouldHideSection; +} diff --git a/frontend/studio-root/pages/hooks/queries/useFetchBelongsToOrgQuery.ts b/frontend/studio-root/pages/hooks/queries/useFetchBelongsToOrgQuery.ts new file mode 100644 index 00000000000..7cfaf9417eb --- /dev/null +++ b/frontend/studio-root/pages/hooks/queries/useFetchBelongsToOrgQuery.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { useServicesContext } from 'app-shared/contexts/ServicesContext'; +import { QueryKey } from 'app-shared/types/QueryKey'; + +export const useFetchBelongsToOrgQuery = () => { + const { fetchBelongsToGiteaOrg } = useServicesContext(); + + return useQuery({ + queryKey: [QueryKey.BelongsToOrg], + queryFn: () => fetchBelongsToGiteaOrg(), + }); +}; From e7c1c9769420b4f2b611f43fb6f9e184e30c5b9e Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 7 Jan 2025 14:27:39 +0100 Subject: [PATCH 03/13] added few tests --- .../pages/Contact/ContactPage.test.tsx | 32 +++++++++++++++---- .../queries/useFetchBelongsToOrgQuery.ts | 8 +++-- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/frontend/studio-root/pages/Contact/ContactPage.test.tsx b/frontend/studio-root/pages/Contact/ContactPage.test.tsx index 09e96ed90af..3f5f280d076 100644 --- a/frontend/studio-root/pages/Contact/ContactPage.test.tsx +++ b/frontend/studio-root/pages/Contact/ContactPage.test.tsx @@ -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', () => { @@ -33,12 +40,6 @@ describe('ContactPage', () => { expect(screen.getByRole('link', { name: textMock('contact.slack.link') })).toBeInTheDocument(); }); - it('should display contact information to "Altinn Servicedesk"', () => { - render(); - expect(screen.getByRole('heading', { name: textMock('contact.altinn_servicedesk.heading') })); - expect(screen.getByText(textMock('contact.altinn_servicedesk.content'))); - }); - it('should display the bug report and feature request section with its content and link', () => { render(); @@ -50,4 +51,23 @@ 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(); + + expect(screen.queryByRole('heading', { name: textMock('contact.altinn_servicedesk.heading') })); + expect(screen.queryByText(textMock('contact.altinn_servicedesk.content'))); + }); + + it('should display contact information to "Altinn Servicedesk"', () => { + (useFetchBelongsToOrgQuery as jest.Mock).mockReturnValue({ + data: { belongsToOrg: true }, + }); + render(); + expect(screen.getByRole('heading', { name: textMock('contact.altinn_servicedesk.heading') })); + expect(screen.getByText(textMock('contact.altinn_servicedesk.content'))); + }); }); diff --git a/frontend/studio-root/pages/hooks/queries/useFetchBelongsToOrgQuery.ts b/frontend/studio-root/pages/hooks/queries/useFetchBelongsToOrgQuery.ts index 7cfaf9417eb..419678942e0 100644 --- a/frontend/studio-root/pages/hooks/queries/useFetchBelongsToOrgQuery.ts +++ b/frontend/studio-root/pages/hooks/queries/useFetchBelongsToOrgQuery.ts @@ -1,8 +1,12 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, type UseQueryResult } from '@tanstack/react-query'; import { useServicesContext } from 'app-shared/contexts/ServicesContext'; import { QueryKey } from 'app-shared/types/QueryKey'; -export const useFetchBelongsToOrgQuery = () => { +type BelongsToOrg = { + belongsToOrg: boolean; +}; + +export const useFetchBelongsToOrgQuery = (): UseQueryResult => { const { fetchBelongsToGiteaOrg } = useServicesContext(); return useQuery({ From 224eae164d3d6c292984b32a21bd9a280e493da8 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 7 Jan 2025 14:38:23 +0100 Subject: [PATCH 04/13] use a DTO --- .../ContactController/GetBelongsToOrgTests.cs | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 backend/tests/Designer.Tests/Controllers/ContactController/GetBelongsToOrgTests.cs diff --git a/backend/tests/Designer.Tests/Controllers/ContactController/GetBelongsToOrgTests.cs b/backend/tests/Designer.Tests/Controllers/ContactController/GetBelongsToOrgTests.cs deleted file mode 100644 index d98373a9e20..00000000000 --- a/backend/tests/Designer.Tests/Controllers/ContactController/GetBelongsToOrgTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Net.Http; -using System.Threading.Tasks; -using Altinn.Studio.Designer.Configuration; -using Designer.Tests.Controllers.ApiTests; -using Microsoft.AspNetCore.Mvc.Testing; -using Xunit; - -namespace Designer.Tests.Controllers.ContactController; - -public class GetBelongsToOrgTests : DesignerEndpointsTestsBase, - IClassFixture> -{ - private const string Org = $"/designer/api/belongs-to-org"; - private static string VersionPrefix(string org, string repository) => $"/designer/api/{org}/{repository}/config"; - - public GetBelongsToOrgTests(WebApplicationFactory factory) : base(factory) - { - } - - [Theory] - public async Task GetBelongsToOrg() - { - string dataPathWithData = VersionPrefix(org, app); - using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, dataPathWithData); - - using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage); - response.EnsureSuccessStatusCode(); - ServiceConfiguration serviceConfigResponse = await response.Content.ReadAsAsync(); - ServiceConfiguration serviceConfiguration = new ServiceConfiguration { RepositoryName = app, ServiceDescription = null, ServiceId = null, ServiceName = null }; - - Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode); - Assert.Equal(serviceConfiguration.RepositoryName, serviceConfigResponse.RepositoryName); - } - -} From dbe7cdc1e91e6ede18be6de0ae108f5f237e5316 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 7 Jan 2025 14:39:28 +0100 Subject: [PATCH 05/13] use a DTO --- backend/src/Designer/Controllers/ContactController.cs | 4 ++-- backend/src/Designer/Models/Dto/BelongsToOrg.cs | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/src/Designer/Controllers/ContactController.cs b/backend/src/Designer/Controllers/ContactController.cs index db1331339e8..2bd6bf3a9f8 100644 --- a/backend/src/Designer/Controllers/ContactController.cs +++ b/backend/src/Designer/Controllers/ContactController.cs @@ -31,11 +31,11 @@ public async Task BelongsToOrg() try { var organizations = await _giteaService.GetUserOrganizations(); - return Ok(new { belongsToOrg = organizations.Count > 0 }); + return Ok(new BelongsToOrgDto{ BelongsToOrg = organizations.Count > 0 }); } catch { - return Ok(new { belongsToOrg = false }); + return Ok(new BelongsToOrgDto{ BelongsToOrg = false }); } } } diff --git a/backend/src/Designer/Models/Dto/BelongsToOrg.cs b/backend/src/Designer/Models/Dto/BelongsToOrg.cs index 2b6038d1a05..c79b96767d0 100644 --- a/backend/src/Designer/Models/Dto/BelongsToOrg.cs +++ b/backend/src/Designer/Models/Dto/BelongsToOrg.cs @@ -1,4 +1,7 @@ -public class BelongsToOrg +using System.Text.Json.Serialization; + +public class BelongsToOrgDto { - public bool belongsToOrg { get; set; } + [JsonPropertyName("belongsToOrg")] + public bool BelongsToOrg { get; set; } } From ce6e47a0910935b611ac7b25ed3f7b6129d4bef3 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 7 Jan 2025 14:48:13 +0100 Subject: [PATCH 06/13] revert formatting --- backend/src/Designer/Program.cs | 60 ++++++++++++++------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/backend/src/Designer/Program.cs b/backend/src/Designer/Program.cs index aaebde1c773..cd668a551f4 100644 --- a/backend/src/Designer/Program.cs +++ b/backend/src/Designer/Program.cs @@ -77,8 +77,7 @@ async Task SetConfigurationProviders(ConfigurationManager config, IWebHostEnviro logger.LogInformation("// Program.cs // SetConfigurationProviders // Attempting to configure providers"); string basePath = Directory.GetParent(Directory.GetCurrentDirectory()).FullName; config.SetBasePath(basePath); - config.AddJsonFile(basePath + "app/altinn-appsettings/altinn-appsettings-secret.json", optional: true, - reloadOnChange: true); + config.AddJsonFile(basePath + "app/altinn-appsettings/altinn-appsettings-secret.json", optional: true, reloadOnChange: true); string envName = hostingEnvironment.EnvironmentName; if (basePath == "/") @@ -87,8 +86,7 @@ async Task SetConfigurationProviders(ConfigurationManager config, IWebHostEnviro } else { - config.AddJsonFile(Directory.GetCurrentDirectory() + "/appsettings.json", optional: false, - reloadOnChange: true); + config.AddJsonFile(Directory.GetCurrentDirectory() + "/appsettings.json", optional: false, reloadOnChange: true); } config.AddEnvironmentVariables(); @@ -103,9 +101,7 @@ async Task SetConfigurationProviders(ConfigurationManager config, IWebHostEnviro !string.IsNullOrEmpty(keyVaultSettings.SecretUri)) { logger.LogInformation("// Program.cs // SetConfigurationProviders // Attempting to configure KeyVault"); - AzureServiceTokenProvider azureServiceTokenProvider = - new( - $"RunAs=App;AppId={keyVaultSettings.ClientId};TenantId={keyVaultSettings.TenantId};AppKey={keyVaultSettings.ClientSecret}"); + AzureServiceTokenProvider azureServiceTokenProvider = new($"RunAs=App;AppId={keyVaultSettings.ClientId};TenantId={keyVaultSettings.TenantId};AppKey={keyVaultSettings.ClientSecret}"); KeyVaultClient keyVaultClient = new( new KeyVaultClient.AuthenticationCallback( azureServiceTokenProvider.KeyVaultTokenCallback)); @@ -127,8 +123,7 @@ async Task SetConfigurationProviders(ConfigurationManager config, IWebHostEnviro if (hostingEnvironment.IsDevelopment() && !Directory.GetCurrentDirectory().Contains("app")) { - config.AddJsonFile(Directory.GetCurrentDirectory() + $"/appsettings.{envName}.json", optional: true, - reloadOnChange: true); + config.AddJsonFile(Directory.GetCurrentDirectory() + $"/appsettings.{envName}.json", optional: true, reloadOnChange: true); Assembly assembly = Assembly.Load(new AssemblyName(hostingEnvironment.ApplicationName)); if (assembly != null) { @@ -156,23 +151,20 @@ void ConfigureLogging(ILoggingBuilder builder) // standalone package Microsoft.Extensions.Logging.ApplicationInsights // or if you want to capture logs from early in the application startup // pipeline from Startup.cs or Program.cs itself. - builder.AddApplicationInsights( - configureTelemetryConfiguration: config => - { - config.ConnectionString = applicationInsightsConnectionString; - }, - configureApplicationInsightsLoggerOptions: _ => { }); + builder.AddApplicationInsights(configureTelemetryConfiguration: config => + { + config.ConnectionString = applicationInsightsConnectionString; + }, + configureApplicationInsightsLoggerOptions: _ => { }); // Optional: Apply filters to control what logs are sent to Application Insights. // The following configures LogLevel Information or above to be sent to // Application Insights for all categories. - builder.AddFilter( - string.Empty, LogLevel.Warning); + builder.AddFilter(string.Empty, LogLevel.Warning); // Adding the filter below to ensure logs of all severity from Program.cs // is sent to ApplicationInsights. - builder.AddFilter( - typeof(Program).FullName, LogLevel.Trace); + builder.AddFilter(typeof(Program).FullName, LogLevel.Trace); } else { @@ -187,16 +179,17 @@ void ConfigureServices(IServiceCollection services, IConfiguration configuration { logger.LogInformation("// Program.cs // ConfigureServices // Attempting to configure services"); - services.Configure(options => { options.AllowSynchronousIO = true; }); + services.Configure(options => + { + options.AllowSynchronousIO = true; + }); - services.ConfigureResourceRegistryIntegrationSettings( - configuration.GetSection("ResourceRegistryIntegrationSettings")); + services.ConfigureResourceRegistryIntegrationSettings(configuration.GetSection("ResourceRegistryIntegrationSettings")); services.ConfigureMaskinportenIntegrationSettings(configuration.GetSection("MaskinportenClientSettings")); services.Configure(configuration.GetSection("MaskinportenClientSettings")); var maskinPortenClientName = "MaskinportenClient"; - services.RegisterMaskinportenClientDefinition(maskinPortenClientName, - configuration.GetSection("MaskinportenClientSettings")); + services.RegisterMaskinportenClientDefinition(maskinPortenClientName, configuration.GetSection("MaskinportenClientSettings")); services.AddHttpClient(); var maskinportenSettings = new MaskinportenClientSettings(); @@ -226,18 +219,14 @@ void ConfigureServices(IServiceCollection services, IConfiguration configuration // Add application insight telemetry if (!string.IsNullOrEmpty(applicationInsightsConnectionString)) { - services.AddApplicationInsightsTelemetry(options => - { - options.ConnectionString = applicationInsightsConnectionString; - }); + services.AddApplicationInsightsTelemetry(options => { options.ConnectionString = applicationInsightsConnectionString; }); services.ConfigureTelemetryModule( (module, o) => { module.Counters.Clear(); module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "threadpool-queue-length")); module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "threadpool-thread-count")); - module.Counters.Add( - new EventCounterCollectionRequest("System.Runtime", "monitor-lock-contention-count")); + module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "monitor-lock-contention-count")); module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "gc-heap-size")); module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "time-in-gc")); module.Counters.Add(new EventCounterCollectionRequest("System.Runtime", "working-set")); @@ -307,7 +296,11 @@ void Configure(IConfiguration configuration) OnPrepareResponse = context => { ResponseHeaders headers = context.Context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromMinutes(60), }; + headers.CacheControl = new CacheControlHeaderValue + { + Public = true, + MaxAge = TimeSpan.FromMinutes(60), + }; } }); @@ -344,11 +337,10 @@ void CreateDirectory(IConfiguration configuration) // TODO: Figure out how appsettings.json parses values and merges with environment variables and use these here. // Since ":" is not valid in environment variables names in kubernetes, we can't use current docker-compose environment variables var repoLocation = Environment.GetEnvironmentVariable("ServiceRepositorySettings:RepositoryLocation") ?? - configuration["ServiceRepositorySettings:RepositoryLocation"]; + configuration["ServiceRepositorySettings:RepositoryLocation"]; if (string.IsNullOrWhiteSpace(repoLocation)) { - repoLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "altinn", - "repos"); + repoLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "altinn", "repos"); configuration.GetSection("ServiceRepositorySettings")["RepositoryLocation"] = repoLocation; } From 900aa3f6afbb0d7117595940d176c99a6d6e7b89 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 7 Jan 2025 14:54:31 +0100 Subject: [PATCH 07/13] format --- .../Designer/Controllers/ContactController.cs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/backend/src/Designer/Controllers/ContactController.cs b/backend/src/Designer/Controllers/ContactController.cs index 2bd6bf3a9f8..14164221a38 100644 --- a/backend/src/Designer/Controllers/ContactController.cs +++ b/backend/src/Designer/Controllers/ContactController.cs @@ -1,41 +1,41 @@ -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 +namespace Altinn.Studio.Designer.Controllers { - private readonly IGitea _giteaService; - - public ContactController(IGitea giteaService) + [Route("designer/api/[controller]")] + [ApiController] + public class ContactController : ControllerBase { - _giteaService = giteaService; - } + private readonly IGitea _giteaService; - [AllowAnonymous] - [HttpGet("belongs-to-org")] - public async Task BelongsToOrg() - { - bool isNotAuthenticated = string.IsNullOrEmpty(AuthenticationHelper.GetDeveloperUserName(HttpContext)); - if (isNotAuthenticated) + public ContactController(IGitea giteaService) { - return Ok(new { belongsToOrg = false }); + _giteaService = giteaService; } - try + [AllowAnonymous] + [HttpGet("belongs-to-org")] + public async Task BelongsToOrg() { - var organizations = await _giteaService.GetUserOrganizations(); - return Ok(new BelongsToOrgDto{ BelongsToOrg = organizations.Count > 0 }); - } - catch - { - return Ok(new BelongsToOrgDto{ BelongsToOrg = false }); + bool isNotAuthenticated = string.IsNullOrEmpty(AuthenticationHelper.GetDeveloperUserName(HttpContext)); + if (isNotAuthenticated) + { + return Ok(new BelongsToOrgDto { BelongsToOrg = false }); + } + + try + { + var organizations = await _giteaService.GetUserOrganizations(); + return Ok(new BelongsToOrgDto { BelongsToOrg = organizations.Count > 0 }); + } + catch + { + return Ok(new BelongsToOrgDto { BelongsToOrg = false }); + } } } } From b9f354539145e73868bc375f9d9107ae41eb3446 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 7 Jan 2025 19:17:12 +0100 Subject: [PATCH 08/13] use isAuthenticated on the HttpContext instead --- backend/src/Designer/Controllers/ContactController.cs | 2 +- backend/src/Designer/Helpers/AuthenticationHelper.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/Designer/Controllers/ContactController.cs b/backend/src/Designer/Controllers/ContactController.cs index 14164221a38..209d05778ef 100644 --- a/backend/src/Designer/Controllers/ContactController.cs +++ b/backend/src/Designer/Controllers/ContactController.cs @@ -21,7 +21,7 @@ public ContactController(IGitea giteaService) [HttpGet("belongs-to-org")] public async Task BelongsToOrg() { - bool isNotAuthenticated = string.IsNullOrEmpty(AuthenticationHelper.GetDeveloperUserName(HttpContext)); + bool isNotAuthenticated = !AuthenticationHelper.IsAuthenticated(HttpContext); if (isNotAuthenticated) { return Ok(new BelongsToOrgDto { BelongsToOrg = false }); diff --git a/backend/src/Designer/Helpers/AuthenticationHelper.cs b/backend/src/Designer/Helpers/AuthenticationHelper.cs index 2675d25ebde..503ee828878 100644 --- a/backend/src/Designer/Helpers/AuthenticationHelper.cs +++ b/backend/src/Designer/Helpers/AuthenticationHelper.cs @@ -23,5 +23,10 @@ public static Task GetDeveloperAppTokenAsync(this HttpContext context) { return context.GetTokenAsync("access_token"); } + + public static bool IsAuthenticated(HttpContext context) + { + return context.User.Identity?.IsAuthenticated ?? false; + } } } From bf999c0020af2c150f3a8af69be918ef0a752b3d Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 7 Jan 2025 20:20:01 +0100 Subject: [PATCH 09/13] added simple test for backend --- .../FetchBelongsToOrgTests.cs | 42 +++++++++++++++++++ .../tests/Designer.Tests/Mocks/IGiteaMock.cs | 9 +++- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 backend/tests/Designer.Tests/Controllers/ContactController/FetchBelongsToOrgTests.cs diff --git a/backend/tests/Designer.Tests/Controllers/ContactController/FetchBelongsToOrgTests.cs b/backend/tests/Designer.Tests/Controllers/ContactController/FetchBelongsToOrgTests.cs new file mode 100644 index 00000000000..26813538161 --- /dev/null +++ b/backend/tests/Designer.Tests/Controllers/ContactController/FetchBelongsToOrgTests.cs @@ -0,0 +1,42 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Altinn.Studio.Designer.Configuration; +using Altinn.Studio.Designer.Services.Interfaces; +using Designer.Tests.Controllers.ApiTests; +using Designer.Tests.Mocks; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Designer.Tests.Controllers.ContactController; + +public class FetchBelongsToOrgTests : DesignerEndpointsTestsBase, + IClassFixture> +{ + public FetchBelongsToOrgTests(WebApplicationFactory factory) : base(factory) + { + } + + protected override void ConfigureTestServices(IServiceCollection services) + { + services.Configure(c => + c.RepositoryLocation = TestRepositoriesLocation); + services.AddSingleton(); + } + + + [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(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(responseContent.BelongsToOrg); + } +} diff --git a/backend/tests/Designer.Tests/Mocks/IGiteaMock.cs b/backend/tests/Designer.Tests/Mocks/IGiteaMock.cs index 37094770ba9..f717e8efbda 100644 --- a/backend/tests/Designer.Tests/Mocks/IGiteaMock.cs +++ b/backend/tests/Designer.Tests/Mocks/IGiteaMock.cs @@ -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 { @@ -131,7 +132,13 @@ public Task> GetTeams() public Task> GetUserOrganizations() { - throw new NotImplementedException(); + var organizations = new List + { + new Organization { Username = "Org1", Id = 1 }, // Example items + new Organization { Username = "Org2", Id = 2 } + }; + + return Task.FromResult(organizations); } public Task> GetUserRepos() From d96358d6529093d49e2fe1238bbb88029a953033 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 7 Jan 2025 20:22:11 +0100 Subject: [PATCH 10/13] PR feedback from code scanning --- backend/src/Designer/Controllers/ContactController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/Designer/Controllers/ContactController.cs b/backend/src/Designer/Controllers/ContactController.cs index 209d05778ef..8083ad78103 100644 --- a/backend/src/Designer/Controllers/ContactController.cs +++ b/backend/src/Designer/Controllers/ContactController.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Altinn.Studio.Designer.Helpers; using Altinn.Studio.Designer.Services.Interfaces; @@ -32,7 +33,7 @@ public async Task BelongsToOrg() var organizations = await _giteaService.GetUserOrganizations(); return Ok(new BelongsToOrgDto { BelongsToOrg = organizations.Count > 0 }); } - catch + catch(Exception) { return Ok(new BelongsToOrgDto { BelongsToOrg = false }); } From 130d63c15c53eff81b54b5c87a1a0bfafce4ffe4 Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Tue, 7 Jan 2025 20:29:09 +0100 Subject: [PATCH 11/13] formatting --- backend/src/Designer/Controllers/ContactController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/Designer/Controllers/ContactController.cs b/backend/src/Designer/Controllers/ContactController.cs index 8083ad78103..c32609a1dd8 100644 --- a/backend/src/Designer/Controllers/ContactController.cs +++ b/backend/src/Designer/Controllers/ContactController.cs @@ -33,7 +33,7 @@ public async Task BelongsToOrg() var organizations = await _giteaService.GetUserOrganizations(); return Ok(new BelongsToOrgDto { BelongsToOrg = organizations.Count > 0 }); } - catch(Exception) + catch (Exception) { return Ok(new BelongsToOrgDto { BelongsToOrg = false }); } From 90ca48740a15fd2612603d0929f85ac53434b1f2 Mon Sep 17 00:00:00 2001 From: David Ovrelid <46874830+framitdavid@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:16:55 +0100 Subject: [PATCH 12/13] Update ContactPage.test.tsx PR feedback Co-authored-by: Nina Kylstad --- frontend/studio-root/pages/Contact/ContactPage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/studio-root/pages/Contact/ContactPage.test.tsx b/frontend/studio-root/pages/Contact/ContactPage.test.tsx index 3f5f280d076..df8a4be8cc0 100644 --- a/frontend/studio-root/pages/Contact/ContactPage.test.tsx +++ b/frontend/studio-root/pages/Contact/ContactPage.test.tsx @@ -62,7 +62,7 @@ describe('ContactPage', () => { expect(screen.queryByText(textMock('contact.altinn_servicedesk.content'))); }); - it('should display contact information to "Altinn Servicedesk"', () => { + it('should display contact information to "Altinn Servicedesk" if user belongs to an org', () => { (useFetchBelongsToOrgQuery as jest.Mock).mockReturnValue({ data: { belongsToOrg: true }, }); From 61fdb78ce9195a1632b75d2a615544f350b54a3c Mon Sep 17 00:00:00 2001 From: "davidovrelid.com" Date: Wed, 8 Jan 2025 11:53:30 +0100 Subject: [PATCH 13/13] added method to queriesMock --- frontend/packages/shared/src/mocks/queriesMock.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/packages/shared/src/mocks/queriesMock.ts b/frontend/packages/shared/src/mocks/queriesMock.ts index 616564b7aa6..0b747d4b18d 100644 --- a/frontend/packages/shared/src/mocks/queriesMock.ts +++ b/frontend/packages/shared/src/mocks/queriesMock.ts @@ -193,6 +193,9 @@ export const queriesMock: ServicesContextProps = { .mockImplementation(() => Promise.resolve([])), updateSelectedMaskinportenScopes: jest.fn().mockImplementation(() => Promise.resolve()), + // Queries - Contact + fetchBelongsToGiteaOrg: jest.fn().mockImplementation(() => Promise.resolve([])), + // Mutations addAppAttachmentMetadata: jest.fn().mockImplementation(() => Promise.resolve()), addDataTypeToAppMetadata: jest.fn().mockImplementation(() => Promise.resolve()),