diff --git a/.github/actions/app-run-local-env/action.yaml b/.github/actions/app-run-local-env/action.yaml index c2902c3d030..c626cc96954 100644 --- a/.github/actions/app-run-local-env/action.yaml +++ b/.github/actions/app-run-local-env/action.yaml @@ -1,16 +1,9 @@ name: "App Run Local Environment" -description: "Starts the frontend server and/or localtest" +description: "Starts the frontend server and optional localtest" inputs: - run-frontend: - description: "Whether to serve frontend with http-server" - required: true run-localtest: description: "Whether to start localtest with studioctl" required: true - frontend-port: - description: "Port for frontend server" - required: false - default: "8080" app-run-targets-json: description: "JSON array of app run targets. Each target requires path and can include image for prebuilt app images." required: false @@ -19,19 +12,10 @@ runs: steps: - name: Validate inputs run: | - if [[ "${{ inputs.run-frontend }}" != "true" && "${{ inputs.run-frontend }}" != "false" ]]; then - echo "Error: run-frontend must be 'true' or 'false', got: ${{ inputs.run-frontend }}" - exit 1 - fi if [[ "${{ inputs.run-localtest }}" != "true" && "${{ inputs.run-localtest }}" != "false" ]]; then echo "Error: run-localtest must be 'true' or 'false', got: ${{ inputs.run-localtest }}" exit 1 fi - # Validate frontend-port is a valid port number (1-65535) - if [[ ! "${{ inputs.frontend-port }}" =~ ^[0-9]+$ ]] || [[ "${{ inputs.frontend-port }}" -lt 1 ]] || [[ "${{ inputs.frontend-port }}" -gt 65535 ]]; then - echo "Error: frontend-port must be a valid port number (1-65535), got: ${{ inputs.frontend-port }}" - exit 1 - fi shell: bash - name: Setup studioctl @@ -60,14 +44,12 @@ runs: studioctl env up --detach - name: Download the built frontend - if: ${{ inputs.run-frontend == 'true' }} uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: app-frontend-dist path: src/App/frontend/dist - name: Setup Node.js - if: ${{ inputs.run-frontend == 'true' }} uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: '22' @@ -75,27 +57,24 @@ runs: cache-dependency-path: yarn.lock - name: Install dependencies - if: ${{ inputs.run-frontend == 'true' }} working-directory: src/App/frontend run: yarn install --immutable shell: bash - name: Start frontend server - if: ${{ inputs.run-frontend == 'true' }} working-directory: src/App/frontend run: | - echo "Starting frontend server on port ${{ inputs.frontend-port }}..." - yarn run serve -p ${{ inputs.frontend-port }} > frontend-server.log 2>&1 & + echo "Starting frontend server on port 8080..." + yarn run serve -p 8080 > frontend-server.log 2>&1 & echo $! > frontend-server.pid echo "Frontend server started with PID $(cat frontend-server.pid)" shell: bash - name: Wait for frontend server to be ready - if: ${{ inputs.run-frontend == 'true' }} run: | echo "Waiting for frontend server to be ready..." - timeout 60 bash -c 'until curl -f http://localhost:${{ inputs.frontend-port }} > /dev/null 2>&1; do sleep 2; done' - echo "Frontend server is ready on http://localhost:${{ inputs.frontend-port }}" + timeout 60 bash -c 'until curl -f http://localhost:8080 > /dev/null 2>&1; do sleep 2; done' + echo "Frontend server is ready on http://localhost:8080" shell: bash - name: Start app containers @@ -121,7 +100,7 @@ runs: } function targetRunArgs(path, image) { - const args = ['run', '--mode', 'container', '--detach', '--random-host-port', '--path', path]; + const args = ['run', '--mode', 'container', '--detach', '--random-host-port', '--dev-frontend', '--path', path]; if (image) { args.push('--image-tag', image, '--pull', '--skip-build'); } diff --git a/.github/workflows/app-frontend-cypress.yml b/.github/workflows/app-frontend-cypress.yml index 74c037c4912..d2a1b5246d3 100644 --- a/.github/workflows/app-frontend-cypress.yml +++ b/.github/workflows/app-frontend-cypress.yml @@ -136,7 +136,6 @@ jobs: - name: Run Local Environment uses: ./.github/actions/app-run-local-env with: - run-frontend: true run-localtest: true app-run-targets-json: ${{ needs.build-app-images.outputs.app-run-targets-json }} @@ -246,7 +245,6 @@ jobs: - name: Run Local Environment uses: ./.github/actions/app-run-local-env with: - run-frontend: true run-localtest: true app-run-targets-json: ${{ steps.app-run-targets.outputs.app-run-targets-json }} diff --git a/.github/workflows/app-frontend-k6-browser.yml b/.github/workflows/app-frontend-k6-browser.yml index bea8f096197..42c466c9362 100644 --- a/.github/workflows/app-frontend-k6-browser.yml +++ b/.github/workflows/app-frontend-k6-browser.yml @@ -46,7 +46,6 @@ jobs: - name: Run Local Environment uses: ./.github/actions/app-run-local-env with: - run-frontend: true run-localtest: true app-run-targets-json: | [{ "path": "src/test/apps/frontend-test" }] diff --git a/.github/workflows/app-frontend-lighthouse-ci.yml b/.github/workflows/app-frontend-lighthouse-ci.yml index be20db42a6f..00e62e08458 100644 --- a/.github/workflows/app-frontend-lighthouse-ci.yml +++ b/.github/workflows/app-frontend-lighthouse-ci.yml @@ -52,7 +52,6 @@ jobs: - name: Run Local Environment uses: ./.github/actions/app-run-local-env with: - run-frontend: true run-localtest: true app-run-targets-json: | [{ "path": "src/test/apps/component-library" }] diff --git a/infra/runtime/apps-config/base/apps-runtime-common-env.yaml b/infra/runtime/apps-config/base/apps-runtime-common-env.yaml index 69fc4f108af..2a5a4634339 100644 --- a/infra/runtime/apps-config/base/apps-runtime-common-env.yaml +++ b/infra/runtime/apps-config/base/apps-runtime-common-env.yaml @@ -11,7 +11,6 @@ data: PlatformSettings__ApiWorkflowEngineEndpoint: http://workflow-engine-app.runtime-workflow-engine-app.svc.cluster.local/api/v1/ GeneralSettings__ExternalAppBaseUrl: https://{org}.apps.{hostName}/{org}/{app}/ PlatformFrontendSettings__PostalCodesUrl: https://altinncdn.no/postcodes/registry.json - PlatformFrontendSettings__AppFrontendCdnBaseUrl: https://altinncdn.no/toolkits/altinn-app-frontend PlatformFrontendSettings__AltinnLogoUrl: https://altinncdn.no/img/Altinn-logo-blue.svg PlatformFrontendSettings__HelpCircleIllustrationUrl: https://altinncdn.no/img/illustration-help-circle.svg --- diff --git a/infra/runtime/apps-config/base/apps-runtime-common.yaml b/infra/runtime/apps-config/base/apps-runtime-common.yaml index dda8de908a4..161c793de1b 100644 --- a/infra/runtime/apps-config/base/apps-runtime-common.yaml +++ b/infra/runtime/apps-config/base/apps-runtime-common.yaml @@ -22,7 +22,6 @@ data: }, "PlatformFrontendSettings": { "PostalCodesUrl": "https://altinncdn.no/postcodes/registry.json", - "AppFrontendCdnBaseUrl": "https://altinncdn.no/toolkits/altinn-app-frontend", "AltinnLogoUrl": "https://altinncdn.no/img/Altinn-logo-blue.svg", "HelpCircleIllustrationUrl": "https://altinncdn.no/img/illustration-help-circle.svg" } diff --git a/libs/form-component/src/app-components/Table/Table.test.tsx b/libs/form-component/src/app-components/Table/Table.test.tsx index 7ebedfde616..06ba7b5a324 100644 --- a/libs/form-component/src/app-components/Table/Table.test.tsx +++ b/libs/form-component/src/app-components/Table/Table.test.tsx @@ -109,13 +109,7 @@ describe('AppTable', () => { it('renders the loading spinner when isLoading is true', () => { render( - , + , ); expect(screen.getByLabelText('Loading rows')).toBeInTheDocument(); }); diff --git a/libs/form-component/src/app-components/Table/Table.tsx b/libs/form-component/src/app-components/Table/Table.tsx index 92084acb841..af17e95a53e 100644 --- a/libs/form-component/src/app-components/Table/Table.tsx +++ b/libs/form-component/src/app-components/Table/Table.tsx @@ -137,10 +137,7 @@ export function AppTable({ colSpan={columns.length + (actionButtons ? 1 : 0)} style={{ textAlign: 'center' }} > - + ) : data.length === 0 ? ( @@ -162,10 +159,7 @@ export function AppTable({ if (col.renderCell) { return ( - + {col.renderCell(cellValues, rowData, rowIndex)} ); @@ -173,10 +167,7 @@ export function AppTable({ if (cellValues.length === 0) { return ( - + - ); @@ -184,20 +175,14 @@ export function AppTable({ if (cellValues.length === 1) { return ( - + {formatValue(cellValues[0])} ); } return ( - +
    {cellValues.map((value, idx) => (
  • {formatValue(value)}
  • diff --git a/src/App/backend/src/Altinn.App.Api/Controllers/HomeController.cs b/src/App/backend/src/Altinn.App.Api/Controllers/HomeController.cs index f0929af1e8f..047761560a3 100644 --- a/src/App/backend/src/Altinn.App.Api/Controllers/HomeController.cs +++ b/src/App/backend/src/Altinn.App.Api/Controllers/HomeController.cs @@ -118,18 +118,17 @@ public async Task Index( return PartialView("Index"); } - string? frontendVersionOverride = null; - if (_env.IsDevelopment() && HttpContext.Request.Cookies.TryGetValue("frontendVersion", out var cookie)) - { - frontendVersionOverride = cookie.TrimEnd('/'); - } + string? appFrontendAssetBaseUrlOverride = + _env.IsDevelopment() && !string.IsNullOrWhiteSpace(_appSettings.AppFrontendAssetBaseUrl) + ? _appSettings.AppFrontendAssetBaseUrl.Trim().TrimEnd('/') + : null; var appGlobalState = await _bootstrapGlobalService.GetGlobalState(_appId.Org, _appId.App, returnUrl, lang); var html = await _indexPageGenerator.Generate( _appId.Org, _appId.App, appGlobalState, - frontendVersionOverride + appFrontendAssetBaseUrlOverride ); return Content(html, "text/html; charset=utf-8"); } diff --git a/src/App/backend/src/Altinn.App.Core/Configuration/AppSettings.cs b/src/App/backend/src/Altinn.App.Core/Configuration/AppSettings.cs index 7a2abfc9746..775c470d14a 100644 --- a/src/App/backend/src/Altinn.App.Core/Configuration/AppSettings.cs +++ b/src/App/backend/src/Altinn.App.Core/Configuration/AppSettings.cs @@ -130,6 +130,15 @@ public class AppSettings public string DefaultBootstrapUrl { get; set; } = "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"; + /// + /// Gets or sets the frontend asset URL used by the generated controller index page. + /// + /// + /// This setting is only honored when the host runs in the Development environment. PDF rendering runs in a + /// container, so avoid loopback URLs such as localhost because they resolve inside the container. + /// + public string? AppFrontendAssetBaseUrl { get; set; } + /// /// Open Id Connect Well known endpoint /// diff --git a/src/App/backend/src/Altinn.App.Core/Configuration/PlatformFrontendSettings.cs b/src/App/backend/src/Altinn.App.Core/Configuration/PlatformFrontendSettings.cs index a4387e051c4..0ba5b383148 100644 --- a/src/App/backend/src/Altinn.App.Core/Configuration/PlatformFrontendSettings.cs +++ b/src/App/backend/src/Altinn.App.Core/Configuration/PlatformFrontendSettings.cs @@ -11,11 +11,6 @@ internal class PlatformFrontendSettings /// public Uri PostalCodesUrl { get; set; } = new("https://altinncdn.no/postcodes/registry.json"); - /// - /// Base URL for the app frontend CDN. - /// - public Uri AppFrontendCdnBaseUrl { get; set; } = new("https://altinncdn.no/toolkits/altinn-app-frontend"); - /// /// URL for the Altinn logo SVG. /// diff --git a/src/App/backend/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs b/src/App/backend/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs index b4cc9a18edc..81b61dd6ac7 100644 --- a/src/App/backend/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs +++ b/src/App/backend/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs @@ -6,8 +6,6 @@ using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Pdf; using Altinn.App.Core.Models.Pdf; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OpenTelemetry.Context.Propagation; @@ -32,8 +30,6 @@ public class PdfGeneratorClient : IPdfGeneratorClient private readonly PdfGeneratorSettings _pdfGeneratorSettings; private readonly PlatformSettings _platformSettings; private readonly IUserTokenProvider _userTokenProvider; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IHostEnvironment _hostEnvironment; private readonly Telemetry? _telemetry; /// @@ -46,8 +42,6 @@ public class PdfGeneratorClient : IPdfGeneratorClient /// /// Links to platform services /// A service able to identify the JWT for currently authenticated user. - /// http context - /// The host environment. /// Telemetry service public PdfGeneratorClient( ILogger logger, @@ -55,8 +49,6 @@ public PdfGeneratorClient( IOptions pdfGeneratorSettings, IOptions platformSettings, IUserTokenProvider userTokenProvider, - IHttpContextAccessor httpContextAccessor, - IHostEnvironment hostEnvironment, Telemetry? telemetry = null ) { @@ -65,8 +57,6 @@ public PdfGeneratorClient( _userTokenProvider = userTokenProvider; _pdfGeneratorSettings = pdfGeneratorSettings.Value; _platformSettings = platformSettings.Value; - _httpContextAccessor = httpContextAccessor; - _hostEnvironment = hostEnvironment; _telemetry = telemetry; } @@ -125,25 +115,6 @@ public async Task GeneratePdf(Uri uri, string? footerContent, Cancellati new PdfGeneratorCookieOptions { Value = _userTokenProvider.GetUserToken(), Domain = uri.Host } ); - if ( - _hostEnvironment.IsDevelopment() - && _httpContextAccessor.HttpContext?.Request.Cookies.TryGetValue("frontendVersion", out var frontendVersion) - == true - && !string.IsNullOrEmpty(frontendVersion) - ) - { - frontendVersion = frontendVersion.Replace("localhost", "host.containers.internal"); - generatorRequest.Cookies.Insert( - 0, - new PdfGeneratorCookieOptions - { - Name = "frontendVersion", - Domain = uri.Host, - Value = frontendVersion, - } - ); - } - string requestContent = JsonSerializer.Serialize(generatorRequest, _jsonSerializerOptions); using StringContent stringContent = new(requestContent, Encoding.UTF8, "application/json"); var httpResponseMessage = await _httpClient.PostAsync(_platformSettings.ApiPdf2Endpoint, stringContent, ct); diff --git a/src/App/backend/src/Altinn.App.Core/Internal/App/IIndexPageGenerator.cs b/src/App/backend/src/Altinn.App.Core/Internal/App/IIndexPageGenerator.cs index b327010657c..70ae2f162f0 100644 --- a/src/App/backend/src/Altinn.App.Core/Internal/App/IIndexPageGenerator.cs +++ b/src/App/backend/src/Altinn.App.Core/Internal/App/IIndexPageGenerator.cs @@ -19,12 +19,12 @@ internal interface IIndexPageGenerator /// The organization identifier. /// The application identifier. /// The bootstrap global state for the app. - /// Optional frontend version URL override (only used in development). + /// Optional app frontend asset base URL override (only used in development). /// The generated HTML content. Task Generate( string org, string app, BootstrapGlobalResponse appGlobalState, - string? frontendVersionOverride = null + string? appFrontendAssetBaseUrlOverride = null ); } diff --git a/src/App/backend/src/Altinn.App.Core/Internal/App/IndexPageGenerator.cs b/src/App/backend/src/Altinn.App.Core/Internal/App/IndexPageGenerator.cs index b5408bc682e..b6ae6aa5dca 100644 --- a/src/App/backend/src/Altinn.App.Core/Internal/App/IndexPageGenerator.cs +++ b/src/App/backend/src/Altinn.App.Core/Internal/App/IndexPageGenerator.cs @@ -39,10 +39,35 @@ public async Task Generate( string org, string app, BootstrapGlobalResponse appGlobalState, - string? frontendVersionOverride = null + string? appFrontendAssetBaseUrl = null ) { - var frontendUrl = frontendVersionOverride ?? "https://altinncdn.no/toolkits/altinn-app-frontend/4"; + if (appFrontendAssetBaseUrl is null) + { + var htmlContentError = $$""" + + + + + + + {{org}} - {{app}} + + + +

    Not implemented yet

    +

    Sorry, loading our built-in frontend is not yet supported. Please build and host frontend from the monorepo code yourself.

    +

    To serve a production-level/faster build with no hot-reloads:

    +
    cd src/App/frontend; yarn build; yarn serve 8080
    +

    To serve a slightly slower build with hot-reloads tailored for development:

    +
    cd src/App/frontend; yarn start
    +

    Then make sure to restart this app with:

    +
    studioctl run --dev-frontend
    + + + """; + return htmlContentError; + } var featureToggles = await _frontendFeatures.GetFrontendFeatures(); var featureTogglesJson = JsonSerializer.Serialize(featureToggles, _jsonSerializerOptions); @@ -71,7 +96,7 @@ public async Task Generate( {{org}} - {{app}} - + {{externalStylesheets}}{{customCssLinks}}
    @@ -81,7 +106,7 @@ public async Task Generate( window.featureToggles = {{featureTogglesJson}}; window.altinnAppGlobalData = {{globalDataJson}}; - + {{externalScripts}}{{customJsScripts}} """; diff --git a/src/App/backend/src/Altinn.App.Core/Internal/Auth/AuthenticationTokenResolver.cs b/src/App/backend/src/Altinn.App.Core/Internal/Auth/AuthenticationTokenResolver.cs index 37935acb80f..8b8ce63668d 100644 --- a/src/App/backend/src/Altinn.App.Core/Internal/Auth/AuthenticationTokenResolver.cs +++ b/src/App/backend/src/Altinn.App.Core/Internal/Auth/AuthenticationTokenResolver.cs @@ -75,7 +75,7 @@ CancellationToken cancellationToken ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); string formattedScopes = MaskinportenClient.GetFormattedScopes(request.Scopes); string url = - $"{_localtestBaseUrl}/Home/GetTestOrgToken?org={appMetadata.Org}&orgNumber=991825827&authenticationLevel=3&scopes={Uri.EscapeDataString(formattedScopes)}"; + $"{_localtestBaseUrl}/Home/GetTestOrgToken?org={appMetadata.Org}&orgNumber=405003309&authenticationLevel=3&scopes={Uri.EscapeDataString(formattedScopes)}"; using var client = _httpClientFactory.CreateClient(); var response = await client.GetAsync(url, cancellationToken); diff --git a/src/App/backend/test/Altinn.App.Api.Tests/Controllers/HomeControllerTest_AppFrontendAssetBaseUrl.cs b/src/App/backend/test/Altinn.App.Api.Tests/Controllers/HomeControllerTest_AppFrontendAssetBaseUrl.cs new file mode 100644 index 00000000000..110a8d1776d --- /dev/null +++ b/src/App/backend/test/Altinn.App.Api.Tests/Controllers/HomeControllerTest_AppFrontendAssetBaseUrl.cs @@ -0,0 +1,80 @@ +using System.Net; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using App.IntegrationTests.Mocks.Services; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Moq; +using Xunit.Abstractions; + +namespace Altinn.App.Api.Tests.Controllers; + +public class HomeControllerTest_AppFrontendAssetBaseUrl : ApiTestBase, IClassFixture> +{ + private const string Org = "tdd"; + private const string App = "contributer-restriction"; + + public HomeControllerTest_AppFrontendAssetBaseUrl( + WebApplicationFactory factory, + ITestOutputHelper outputHelper + ) + : base(factory, outputHelper) + { + OverrideEnvironment = Environments.Development; + SendAsync = _ => + Task.FromResult( + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("""{"orgs":{}}""") } + ); + } + + [Fact] + public async Task Index_UsesAppFrontendAssetBaseUrlAppSetting() + { + OverrideAppSetting("AppSettings:AppFrontendAssetBaseUrl", "/configured/frontend/"); + + using var client = GetRootedClient(Org, App, configureServices: ConfigureStatelessAnonymousApp); + using var response = await client.GetAsync($"{Org}/{App}/"); + var html = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("href=\"/configured/frontend/altinn-app-frontend.css\"", html); + Assert.Contains("src=\"/configured/frontend/altinn-app-frontend.js\"", html); + } + + [Fact] + public async Task Index_FailsByDefault() + { + using var client = GetRootedClient(Org, App, configureServices: ConfigureStatelessAnonymousApp); + using var response = await client.GetAsync($"{Org}/{App}/"); + var html = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("loading our built-in frontend is not yet supported", html); + } + + private static void ConfigureStatelessAnonymousApp(IServiceCollection services) + { + var webHostEnvironmentMock = new Mock(); + webHostEnvironmentMock.SetupGet(e => e.EnvironmentName).Returns(Environments.Development); + webHostEnvironmentMock.SetupGet(e => e.ApplicationName).Returns("Altinn.App.Api"); + webHostEnvironmentMock.SetupGet(e => e.ContentRootPath).Returns(Directory.GetCurrentDirectory()); + webHostEnvironmentMock + .SetupGet(e => e.WebRootPath) + .Returns(Path.Join(Directory.GetCurrentDirectory(), "wwwroot")); + webHostEnvironmentMock.SetupGet(e => e.ContentRootFileProvider).Returns(new NullFileProvider()); + webHostEnvironmentMock.SetupGet(e => e.WebRootFileProvider).Returns(new NullFileProvider()); + services.Replace(ServiceDescriptor.Singleton(webHostEnvironmentMock.Object)); + + services.AddSingleton( + new AppMetadataMutationHook(appMetadata => + { + appMetadata.OnEntry = new OnEntry { Show = "Task_1" }; + appMetadata.DataTypes.Find(d => d.Id == "default")!.AppLogic!.AllowAnonymousOnStateless = true; + }) + ); + } +} diff --git a/src/App/backend/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs b/src/App/backend/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs index 394f03ca742..1c326a6ecce 100644 --- a/src/App/backend/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs +++ b/src/App/backend/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs @@ -14,7 +14,6 @@ using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; @@ -104,102 +103,18 @@ public async Task Request_In_Dev_Should_Generate() var httpContextAccessor = new Mock(); httpContextAccessor.Setup(x => x.HttpContext!.Request!.Query["lang"]).Returns(LanguageConst.Nb); - string? frontendVersion = null; - httpContextAccessor - .Setup(x => x.HttpContext!.Request!.Cookies.TryGetValue("frontendVersion", out frontendVersion)) - .Returns(false); var handler = new Mock(); var httpClient = new HttpClient(handler.Object); var logger = new Mock>(); - var hostEnvironment = new Mock(); - hostEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Development); var pdfGeneratorClient = new PdfGeneratorClient( logger.Object, httpClient, _pdfGeneratorSettingsOptions, _platformSettingsOptions, - _userTokenProvider.Object, - httpContextAccessor.Object, - hostEnvironment.Object - ); - var pdfService = NewPdfService(httpContextAccessor, pdfGeneratorClient, generalSettingsOptions); - var pdfController = new PdfController( - _instanceClient.Object, - _pdfFormatter.Object, - _appResources.Object, - _appModel.Object, - _dataClient.Object, - pdfService - ); - - string? requestBody = null; - using ( - var mockResponse = new HttpResponseMessage() - { - StatusCode = System.Net.HttpStatusCode.OK, - Content = new StringContent("PDF"), - } - ) - { - handler - .Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny() - ) - .Returns( - async (m, c) => - { - requestBody = await m.Content!.ReadAsStringAsync(); - return mockResponse; - } - ); - - var result = await pdfController.GetPdfPreview(_org, _app, _partyId, _instanceId); - result.Should().BeOfType(typeof(FileStreamResult)); - } - - requestBody - .Should() - .Contain( - @"url"":""http://local.altinn.cloud/org/app/instance/12345/e11e3e0b-a45c-48fb-a968-8d4ddf868c80?pdf=1" - ); - requestBody.Should().NotContain(@"name"":""frontendVersion"); - } - - [Fact] - public async Task Request_In_Dev_Should_Include_Frontend_Version() - { - IOptions generalSettingsOptions = Options.Create( - new() { HostName = "local.altinn.cloud" } - ); - - var httpContextAccessor = new Mock(); - httpContextAccessor.Setup(x => x.HttpContext!.Request!.Query["lang"]).Returns(LanguageConst.Nb); - string? frontendVersion = "https://altinncdn.no/toolkits/altinn-app-frontend/3/"; - httpContextAccessor - .Setup(x => x.HttpContext!.Request!.Cookies.TryGetValue("frontendVersion", out frontendVersion)) - .Returns(true); - - var handler = new Mock(); - var httpClient = new HttpClient(handler.Object); - - var logger = new Mock>(); - var hostEnvironment = new Mock(); - hostEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Development); - - var pdfGeneratorClient = new PdfGeneratorClient( - logger.Object, - httpClient, - _pdfGeneratorSettingsOptions, - _platformSettingsOptions, - _userTokenProvider.Object, - httpContextAccessor.Object, - hostEnvironment.Object + _userTokenProvider.Object ); var pdfService = NewPdfService(httpContextAccessor, pdfGeneratorClient, generalSettingsOptions); var pdfController = new PdfController( @@ -244,13 +159,10 @@ public async Task Request_In_Dev_Should_Include_Frontend_Version() .Contain( @"url"":""http://local.altinn.cloud/org/app/instance/12345/e11e3e0b-a45c-48fb-a968-8d4ddf868c80?pdf=1" ); - requestBody - .Should() - .Contain(@"name"":""frontendVersion"",""value"":""https://altinncdn.no/toolkits/altinn-app-frontend/3/"""); } [Fact] - public async Task Request_In_TT02_Should_Ignore_Frontend_Version() + public async Task Request_In_TT02_Should_Generate() { IOptions generalSettingsOptions = Options.Create( new() { HostName = "org.apps.tt02.altinn.no" } @@ -258,26 +170,18 @@ public async Task Request_In_TT02_Should_Ignore_Frontend_Version() var httpContextAccessor = new Mock(); httpContextAccessor.Setup(x => x.HttpContext!.Request!.Query["lang"]).Returns(LanguageConst.Nb); - string? frontendVersion = "https://altinncdn.no/toolkits/altinn-app-frontend/3/"; - httpContextAccessor - .Setup(x => x.HttpContext!.Request!.Cookies.TryGetValue("frontendVersion", out frontendVersion)) - .Returns(true); var handler = new Mock(); var httpClient = new HttpClient(handler.Object); var logger = new Mock>(); - var hostEnvironment = new Mock(); - hostEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Production); var pdfGeneratorClient = new PdfGeneratorClient( logger.Object, httpClient, _pdfGeneratorSettingsOptions, _platformSettingsOptions, - _userTokenProvider.Object, - httpContextAccessor.Object, - hostEnvironment.Object + _userTokenProvider.Object ); var pdfService = NewPdfService(httpContextAccessor, pdfGeneratorClient, generalSettingsOptions); var pdfController = new PdfController( @@ -322,6 +226,5 @@ public async Task Request_In_TT02_Should_Ignore_Frontend_Version() .Contain( @"url"":""http://org.apps.tt02.altinn.no/org/app/instance/12345/e11e3e0b-a45c-48fb-a968-8d4ddf868c80?pdf=1" ); - requestBody.Should().NotContain(@"name"":""frontendVersion"); } } diff --git a/src/App/backend/test/Altinn.App.Core.Tests/Internal/Auth/AuthenticationTokenResolverTest.cs b/src/App/backend/test/Altinn.App.Core.Tests/Internal/Auth/AuthenticationTokenResolverTest.cs index 8ce229c1e0d..dee997eb637 100644 --- a/src/App/backend/test/Altinn.App.Core.Tests/Internal/Auth/AuthenticationTokenResolverTest.cs +++ b/src/App/backend/test/Altinn.App.Core.Tests/Internal/Auth/AuthenticationTokenResolverTest.cs @@ -116,7 +116,7 @@ public async Task GetAccessToken_CallsCorrectUrl_Localtest() var authMethodAltinn = AuthenticationMethod.ServiceOwner("a", "b"); string requestedUrl = string.Empty; string expectedUrl = - "http://localhost:5101/Home/GetTestOrgToken?org=test-org&orgNumber=991825827&authenticationLevel=3&scopes=altinn%3Aserviceowner%20altinn%3Aserviceowner%2Finstances.read%20altinn%3Aserviceowner%2Finstances.write%20a%20b"; + "http://localhost:5101/Home/GetTestOrgToken?org=test-org&orgNumber=405003309&authenticationLevel=3&scopes=altinn%3Aserviceowner%20altinn%3Aserviceowner%2Finstances.read%20altinn%3Aserviceowner%2Finstances.write%20a%20b"; await using var fixture = Fixture.Create( _generalSettingsLocal, diff --git a/src/App/backend/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/src/App/backend/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index 11b211074fd..1a738414a9a 100644 --- a/src/App/backend/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/src/App/backend/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -93,15 +93,12 @@ public async Task ValidRequest_ShouldReturnPdf() var httpClient = new HttpClient(delegatingHandler); var logger = new Mock>(); - var hostEnvironment = new Mock(); var pdfGeneratorClient = new PdfGeneratorClient( logger.Object, httpClient, _pdfGeneratorSettingsOptions, _platformSettingsOptions, - _userTokenProvider.Object, - _httpContextAccessor.Object, - hostEnvironment.Object + _userTokenProvider.Object ); Stream pdf = await pdfGeneratorClient.GeneratePdf( @@ -125,15 +122,12 @@ public async Task ValidRequest_PdfGenerationFails_ShouldThrowException() var httpClient = new HttpClient(delegatingHandler); var logger = new Mock>(); - var hostEnvironment = new Mock(); var pdfGeneratorClient = new PdfGeneratorClient( logger.Object, httpClient, _pdfGeneratorSettingsOptions, _platformSettingsOptions, - _userTokenProvider.Object, - _httpContextAccessor.Object, - hostEnvironment.Object + _userTokenProvider.Object ); var func = async () => diff --git a/src/App/backend/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/src/App/backend/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index ee78d0ff9b6..740bcfa2e77 100644 --- a/src/App/backend/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/src/App/backend/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -8,6 +8,7 @@ namespace Altinn.App.Core.Configuration public const string VALIDATION_CONFIG_FILENAME = "validation.json"; public AppSettings() { } public string AppBasePath { get; set; } + public string? AppFrontendAssetBaseUrl { get; set; } public string AppOidcProvider { get; set; } public string AppVersion { get; set; } public string ApplicationMetadataFileName { get; set; } @@ -2576,7 +2577,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Pdf { public class PdfGeneratorClient : Altinn.App.Core.Internal.Pdf.IPdfGeneratorClient { - public PdfGeneratorClient(Microsoft.Extensions.Logging.ILogger logger, System.Net.Http.HttpClient httpClient, Microsoft.Extensions.Options.IOptions pdfGeneratorSettings, Microsoft.Extensions.Options.IOptions platformSettings, Altinn.App.Core.Internal.Auth.IUserTokenProvider userTokenProvider, Microsoft.AspNetCore.Http.IHttpContextAccessor httpContextAccessor, Microsoft.Extensions.Hosting.IHostEnvironment hostEnvironment, Altinn.App.Core.Features.Telemetry? telemetry = null) { } + public PdfGeneratorClient(Microsoft.Extensions.Logging.ILogger logger, System.Net.Http.HttpClient httpClient, Microsoft.Extensions.Options.IOptions pdfGeneratorSettings, Microsoft.Extensions.Options.IOptions platformSettings, Altinn.App.Core.Internal.Auth.IUserTokenProvider userTokenProvider, Altinn.App.Core.Features.Telemetry? telemetry = null) { } public System.Threading.Tasks.Task GeneratePdf(System.Uri uri, System.Threading.CancellationToken ct) { } public System.Threading.Tasks.Task GeneratePdf(System.Uri uri, string? footerContent, System.Threading.CancellationToken ct) { } } diff --git a/src/App/backend/test/Altinn.App.Integration.Tests/_fixture/AppFixture.cs b/src/App/backend/test/Altinn.App.Integration.Tests/_fixture/AppFixture.cs index e2005811b30..956c3ad7011 100644 --- a/src/App/backend/test/Altinn.App.Integration.Tests/_fixture/AppFixture.cs +++ b/src/App/backend/test/Altinn.App.Integration.Tests/_fixture/AppFixture.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Net; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; @@ -161,10 +160,12 @@ await WriteFixtureConfiguration( fixtureInstance, cancellationToken ); + var appFrontendAssetBaseUrl = $"/{appId}/altinn-app-frontend"; appProcess = await StudioctlAppProcess.Start( generatedAppDirectory, fixtureConfigurationPath, _nugetPackagesDirectory, + appFrontendAssetBaseUrl, logger, cancellationToken ); @@ -219,17 +220,11 @@ public HttpClient GetAppClient() { if (_appClient == null) { - var cookieContainer = new CookieContainer(); - var handler = new HttpClientHandler { CookieContainer = cookieContainer }; - - _appClient = new HttpClient(handler) + _appClient = new HttpClient { BaseAddress = new Uri($"http://local.altinn.cloud:{StudioctlLocaltestHostPort}"), }; _appClient.DefaultRequestHeaders.Add("User-Agent", "Altinn.App.Integration.Tests"); - - var appFrontendUrl = $"{AppPath}/altinn-app-frontend/"; - cookieContainer.Add(_appClient.BaseAddress, new Cookie("frontendVersion", appFrontendUrl)); } return _appClient; diff --git a/src/App/backend/test/Altinn.App.Integration.Tests/_fixture/StudioctlEnvironment.cs b/src/App/backend/test/Altinn.App.Integration.Tests/_fixture/StudioctlEnvironment.cs index 5d58e1a633f..c32b5f37ec8 100644 --- a/src/App/backend/test/Altinn.App.Integration.Tests/_fixture/StudioctlEnvironment.cs +++ b/src/App/backend/test/Altinn.App.Integration.Tests/_fixture/StudioctlEnvironment.cs @@ -236,6 +236,7 @@ public static async Task Start( string appDirectory, string fixtureConfigurationPath, string nugetPackagesDirectory, + string appFrontendAssetBaseUrl, ILogger logger, CancellationToken cancellationToken ) @@ -246,6 +247,7 @@ CancellationToken cancellationToken nugetPackagesDirectory, logger, cancellationToken, + appFrontendAssetBaseUrl, "run", "--mode", "process", @@ -282,6 +284,7 @@ await RunStudioctl( nugetPackagesDirectory: null, logger, CancellationToken.None, + appFrontendAssetBaseUrl: null, "app", "stop", "--path", @@ -404,6 +407,7 @@ private static async Task RunStudioctl( string? nugetPackagesDirectory, ILogger logger, CancellationToken cancellationToken, + string? appFrontendAssetBaseUrl, params string[] arguments ) { @@ -418,6 +422,8 @@ params string[] arguments process.StartInfo.Environment["AppFixture__ConfigurationPath"] = fixtureConfigurationPath; if (nugetPackagesDirectory is not null) process.StartInfo.Environment["NUGET_PACKAGES"] = nugetPackagesDirectory; + if (appFrontendAssetBaseUrl is not null) + process.StartInfo.Environment["AppSettings__AppFrontendAssetBaseUrl"] = appFrontendAssetBaseUrl; foreach (var argument in arguments) process.StartInfo.ArgumentList.Add(argument); diff --git a/src/App/frontend/monorepo-changed-paths.txt b/src/App/frontend/monorepo-changed-paths.txt index 912dff5a567..1614508de19 100644 --- a/src/App/frontend/monorepo-changed-paths.txt +++ b/src/App/frontend/monorepo-changed-paths.txt @@ -42,6 +42,7 @@ src/features/datamodel/DataElementIdsForCypress.tsx src/features/datamodel/DataModelsProvider.tsx src/features/datamodel/useDataModelSchemaQuery.ts src/features/datamodel/utils.ts +src/features/devtools/components/VersionSwitcher/VersionSwitcher.tsx src/features/entrypoint/Entrypoint.tsx src/features/form/FormContext.tsx src/features/form/dynamics/ @@ -85,7 +86,7 @@ src/utils/formLayout.test.ts src/utils/formLayout.ts src/utils/layout/generator/GeneratorDataSources.tsx src/utils/refs/ -src/utils/versioning/versions.ts +src/utils/versioning/ test/e2e/config/ test/e2e/integration/component-library/pdf.ts test/e2e/integration/frontend-test/language.ts diff --git a/src/App/frontend/src/features/devtools/DevToolsControls.tsx b/src/App/frontend/src/features/devtools/DevToolsControls.tsx index 2101e9161e0..8d4362e213c 100644 --- a/src/App/frontend/src/features/devtools/DevToolsControls.tsx +++ b/src/App/frontend/src/features/devtools/DevToolsControls.tsx @@ -10,13 +10,10 @@ import { DevToolsLogs } from 'src/features/devtools/components/DevToolsLogs/DevT import { DownloadXMLButton } from 'src/features/devtools/components/DownloadXMLButton/DownloadXMLButton'; import { ExpressionPlayground } from 'src/features/devtools/components/ExpressionPlayground/ExpressionPlayground'; import { ComponentSelector } from 'src/features/devtools/components/LayoutInspector/ComponentSelector'; -// There are no beta features at this time -// import { FeatureToggles } from 'src/features/devtools/components/FeatureToggles/FeatureToggles'; import { LayoutInspector } from 'src/features/devtools/components/LayoutInspector/LayoutInspector'; import { NodeInspector } from 'src/features/devtools/components/NodeInspector/NodeInspector'; import { PDFPreviewButton } from 'src/features/devtools/components/PDFPreviewButton/PDFPreviewButton'; import { PermissionsEditor } from 'src/features/devtools/components/PermissionsEditor/PermissionsEditor'; -import { VersionSwitcher } from 'src/features/devtools/components/VersionSwitcher/VersionSwitcher'; import { useDevToolsStore } from 'src/features/devtools/data/DevToolsStore'; import { DevToolsTab } from 'src/features/devtools/data/types'; import classes from 'src/features/devtools/DevTools.module.css'; @@ -60,7 +57,6 @@ export const DevToolsControls = () => { - @@ -97,11 +93,6 @@ export const DevToolsControls = () => { )} - { - // - // - // - } ); }; diff --git a/src/App/frontend/src/features/devtools/components/VersionSwitcher/VersionSwitcher.tsx b/src/App/frontend/src/features/devtools/components/VersionSwitcher/VersionSwitcher.tsx deleted file mode 100644 index 098952bad39..00000000000 --- a/src/App/frontend/src/features/devtools/components/VersionSwitcher/VersionSwitcher.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; - -import { Button, Spinner } from '@app/form-component'; -import { EXPERIMENTAL_Suggestion as Suggestion, Fieldset } from '@digdir/designsystemet-react'; -import { useQuery } from '@tanstack/react-query'; -import axios from 'axios'; - -import comboboxClasses from 'src/styles/combobox.module.css'; -import { optionFilter } from 'src/utils/options'; -import { appPath, getAppFrontendCDNPath, getFrontendVersionsCDN } from 'src/utils/urls/appUrlHelper'; - -export const VersionSwitcher = () => { - const [selectedVersion, setSelectedVersion] = React.useState(undefined); - const { - data: versions, - isLoading: isVersionsLoading, - isError: isVersionsError, - } = useQuery({ - queryKey: ['frontendVersions'], - queryFn: () => axios.get(getFrontendVersionsCDN()).then((res) => res.data), - select: (data: string[]) => [ - { label: 'localhost:8080', value: 'http://localhost:8080' }, - ...data - .slice() - .reverse() - .map((v) => ({ label: v, value: `${getAppFrontendCDNPath()}/${v}` })), - ], - }); - - const { - data: html, - isLoading: isHtmlLoading, - isError: isHtmlError, - } = useQuery({ - queryKey: ['indexHtml'], - queryFn: () => axios.get(appPath).then((res) => res.data), - }); - - const onClick = () => { - if (selectedVersion) { - const newDoc = html - .replace(/src=".*\/altinn-app-frontend.js"/, `src="${selectedVersion}/altinn-app-frontend.js"`) - .replace(/href=".*\/altinn-app-frontend.css"/, `href="${selectedVersion}/altinn-app-frontend.css"`); - document.open(); - document.write(newDoc); - document.close(); - } - }; - - if (isVersionsLoading || isHtmlLoading || !versions) { - return ; - } - - if (isVersionsError || isHtmlError) { - return

    Det skjedde en feil ved henting av versjoner

    ; - } - - const foundVersion = versions.find((v) => v.value === selectedVersion); - - return ( -
    - Frontend versjon - - - - Ingen versjoner funnet - {versions.map((version) => ( - setSelectedVersion(version.value)} - > - {version.label} - - ))} - - - {selectedVersion && Last inn siden på nytt for å gå tilbake til opprinnelig versjon.} - -
    - ); -}; diff --git a/src/App/frontend/src/setupTests.ts b/src/App/frontend/src/setupTests.ts index 55929285e0f..f627f713305 100644 --- a/src/App/frontend/src/setupTests.ts +++ b/src/App/frontend/src/setupTests.ts @@ -78,7 +78,6 @@ beforeEach(() => { frontendSettings: getApplicationSettingsMock(), platformFrontendSettings: { postalCodesUrl: 'https://altinncdn.no/postcodes/registry.json', - appFrontendCdnBaseUrl: 'https://altinncdn.no/toolkits/altinn-app-frontend', altinnLogoUrl: 'https://altinncdn.no/img/Altinn-logo-blue.svg', helpCircleIllustrationUrl: 'https://altinncdn.no/img/illustration-help-circle.svg', }, diff --git a/src/App/frontend/src/types/shared.ts b/src/App/frontend/src/types/shared.ts index 4572ec69c8b..e8839dbce02 100644 --- a/src/App/frontend/src/types/shared.ts +++ b/src/App/frontend/src/types/shared.ts @@ -287,7 +287,6 @@ export interface IApplicationSettings { } export interface IPlatformFrontendSettings { - appFrontendCdnBaseUrl: string; altinnLogoUrl: string; helpCircleIllustrationUrl: string; postalCodesUrl: string; diff --git a/src/App/frontend/src/utils/urls/appUrlHelper.ts b/src/App/frontend/src/utils/urls/appUrlHelper.ts index 734c399d0b3..7c6aeee9739 100644 --- a/src/App/frontend/src/utils/urls/appUrlHelper.ts +++ b/src/App/frontend/src/utils/urls/appUrlHelper.ts @@ -145,8 +145,6 @@ export const redirectToUpgrade = (reqAuthLevel: string) => { export const getActiveInstancesUrl = (partyId: number) => `${appPath}/instances/${partyId}/active`; export const getInstanceUiUrl = (instanceId: string) => `${appPath}/instance/${instanceId}`; -export const getAppFrontendCDNPath = () => GlobalData.platformFrontendSettings.appFrontendCdnBaseUrl; -export const getFrontendVersionsCDN = () => `${getAppFrontendCDNPath()}/index.json`; export const getHelpCircleIllustrationUrl = () => GlobalData.platformFrontendSettings.helpCircleIllustrationUrl; export type ParamValue = string | number | boolean | null; diff --git a/src/App/frontend/src/utils/versioning/versionCompare.test.ts b/src/App/frontend/src/utils/versioning/versionCompare.test.ts deleted file mode 100644 index 6c597c650e5..00000000000 --- a/src/App/frontend/src/utils/versioning/versionCompare.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { isAtLeastVersion } from 'src/utils/versioning/versionCompare'; - -describe('versionCompare', () => { - interface TestCase { - version: string; - minVersion: string; - expected: boolean; - } - - const testCases: TestCase[] = [ - { version: '1.0.0', minVersion: '1.0.0', expected: true }, - { version: '1.0.0', minVersion: '1.0.1', expected: true }, - { version: '1.0.0', minVersion: '0.9.9', expected: true }, - { version: '8.0.0', minVersion: '7.9.9', expected: true }, - { version: '8.0.15', minVersion: '8.0.14', expected: true }, - { version: '8.1.15', minVersion: '8.0.16', expected: true }, - { version: '8.1.15', minVersion: '8.1.14', expected: true }, - { version: '8.1.15', minVersion: '8.1.15', expected: true }, - { version: '8.1.15', minVersion: '8.1.16', expected: false }, - { version: '8.1.15', minVersion: '8.2.14', expected: false }, - { version: '8.1.15', minVersion: '9.0.0', expected: false }, - ]; - - test.each(testCases)('isAtLeastVersion($version, $minVersion) returns $expected', (testCase) => { - expect(isAtLeastVersion({ actualVersion: testCase.version, minimumVersion: testCase.minVersion })).toBe( - testCase.expected, - ); - }); - - test('should allow zero in last part', () => { - expect(isAtLeastVersion({ actualVersion: '1.2.0', minimumVersion: '1.2.3' })).toBe(true); - }); -}); diff --git a/src/App/frontend/src/utils/versioning/versionCompare.ts b/src/App/frontend/src/utils/versioning/versionCompare.ts deleted file mode 100644 index 8617e6e1b88..00000000000 --- a/src/App/frontend/src/utils/versioning/versionCompare.ts +++ /dev/null @@ -1,32 +0,0 @@ -interface VersionCompareProps { - actualVersion: string; - minimumVersion: string; -} - -/** - * Checks if the given version is at least the given minimum version. Expects the version numbers to be - * dot-separated numbers, e.g. "1.0.15". String parts, such as 'preview', 'alpha', 'rc' are not supported. - * - * The last part of the version is allowed to be zero. This is useful - * when running the backend with project references, as the build number is set to zero in that case. - */ -export function isAtLeastVersion({ actualVersion, minimumVersion }: VersionCompareProps): boolean { - const parts = actualVersion.split('.'); - const expectedParts = minimumVersion.split('.'); - if (parts.length !== expectedParts.length) { - return false; - } - for (const i in expectedParts) { - const expected = parseInt(expectedParts[i], 10); - const actual = parseInt(parts[i], 10); - const isLast = parseInt(i) === expectedParts.length - 1; - if (isLast && actual === 0) { - return true; - } else if (actual > expected) { - return true; - } else if (actual < expected) { - return false; - } - } - return true; -} diff --git a/src/App/frontend/src/utils/versioning/versions.ts b/src/App/frontend/src/utils/versioning/versions.ts deleted file mode 100644 index 0889cd0e6fa..00000000000 --- a/src/App/frontend/src/utils/versioning/versions.ts +++ /dev/null @@ -1 +0,0 @@ -export const MINIMUM_APPLICATION_VERSION_NAME = 'v8.0.0'; diff --git a/src/App/frontend/template.env b/src/App/frontend/template.env index bf35acefade..e72814a6d5d 100644 --- a/src/App/frontend/template.env +++ b/src/App/frontend/template.env @@ -31,11 +31,6 @@ CYPRESS_RECORD_VIDEO=false ## Set this to false to disable compression (speeds up test duration, but will increase disk usage) CYPRESS_VIDEO_COMPRESSION=32 -## You can also override the host used by Cypress to load css/js files. This defaults to localhost:8080, -## and can also be configured explicitly when calling Cypress by using the --env host= flag. -## See start-app-instance.ts for more details. -#CYPRESS_HOST=localhost:8080 - ## You can override the window size for Cypress when running headless. This defaults to 1920x1080. CYPRESS_WINDOW_WIDTH=1920 CYPRESS_WINDOW_HEIGHT=1080 diff --git a/src/App/frontend/test/e2e/config/localtest.json b/src/App/frontend/test/e2e/config/localtest.json index cc8f65ad89c..e2a3f23148d 100644 --- a/src/App/frontend/test/e2e/config/localtest.json +++ b/src/App/frontend/test/e2e/config/localtest.json @@ -1,7 +1,6 @@ { "$schema": "https://on.cypress.io/cypress.schema.json", "baseUrl": "http://local.altinn.cloud:8000", - "frontendUrl": "http://app-frontend.local.altinn.cloud:8000", "env": { "type": "localtest", diff --git a/src/App/frontend/test/e2e/config/tt02.json b/src/App/frontend/test/e2e/config/tt02.json index ac4902042f0..7ecaa55f870 100644 --- a/src/App/frontend/test/e2e/config/tt02.json +++ b/src/App/frontend/test/e2e/config/tt02.json @@ -1,7 +1,6 @@ { "$schema": "https://on.cypress.io/cypress.schema.json", "baseUrl": "https://tt02.altinn.no", - "frontendUrl": "http://localhost:8080", "env": { "type": "production-like", diff --git a/src/App/frontend/test/e2e/support/start-app-instance.ts b/src/App/frontend/test/e2e/support/start-app-instance.ts index 0f3c8539d8a..6bd95895982 100644 --- a/src/App/frontend/test/e2e/support/start-app-instance.ts +++ b/src/App/frontend/test/e2e/support/start-app-instance.ts @@ -1,4 +1,3 @@ -import dotenv from 'dotenv'; import escapeRegex from 'escape-string-regexp'; import { cyUserLogin, tenorUserLogin } from 'test/e2e/support/auth'; @@ -12,18 +11,8 @@ Cypress.Commands.add('startAppInstance', function (appName, options) { urlSuffix = '', authenticationLevel = '1', } = options || {}; - const env = dotenv.config({ quiet: true }).parsed || {}; cy.log(`Starting app instance: ${appName}`); - // You can override the host we load css/js from, using multiple methods: - // 1. Start Cypress with --env environment=,host= - // 2. Set CYPRESS_HOST= in your .env file - // This is useful, for example if you want to run a Cypress test locally in the background while working on - // other things. Build the app-frontend with `yarn build` and serve it with `yarn serve 8081`, then run - // Cypress using a command like this: - // npx cypress run --env environment=tt02,host=localhost:8081 -s 'test/e2e/integration/*/*.ts' - const targetHost = Cypress.env('host') || env.CYPRESS_HOST || getConfiguredFrontendHost(); - const visitOptions: Partial = { onBeforeLoad: (win) => { const wrap = @@ -79,11 +68,6 @@ Cypress.Commands.add('startAppInstance', function (appName, options) { // This mechanism lets you override what happens in the @app response, for example in a login handler. cy.wrap({ current: undefined }).as('appResponse'); - // Rewrite all references to the app-frontend with a local URL - // We cannot just intercept and redirect (like we did before), because Percy reads this DOM to figure out where - // to download assets from. If we redirect, Percy will download from altinncdn.no, which will cause the test to - // use outdated CSS. - // https://docs.percy.io/docs/debugging-sdks#asset-discovery cy.get('@appResponse').then((ref) => { cy.intercept({ url: targetUrl }, (req) => { const cookies = req.headers['cookie'] || ''; @@ -96,24 +80,11 @@ Cypress.Commands.add('startAppInstance', function (appName, options) { if (evaluateBefore && !cookies.includes('cy-evaluated-js=true')) { res.body = generateHtmlToEval(evaluateBefore); - return; } - - const source = /https?:\/\/.*?\/altinn-app-frontend\./g; - const target = `http://${targetHost}/altinn-app-frontend.`; - res.body = res.body.replace(source, target); } }); }).as('app'); }); - - cy.intercept('GET', `http://${targetHost}/altinn-app-frontend.js`).as('js'); - cy.intercept('GET', `http://${targetHost}/altinn-app-frontend.css`).as('css'); - - cy.intercept('https://altinncdn.no/toolkits/altinn-app-frontend/*/altinn-app-frontend.*', (req) => { - req.destroy(); - throw new Error('Requested asset from altinncdn.no, our rewrite code is apparently not working, aborting test'); - }); } if (Cypress.env('type') === 'localtest') { @@ -131,26 +102,11 @@ Cypress.Commands.add('startAppInstance', function (appName, options) { cyUserLogin({ cyUser, authenticationLevel, appName }); } - // Setting frontendVersion cookie to make sure we use the target frontend version for the PDF generation as well - const domain = new URL(Cypress.config().baseUrl!).hostname; - cy.setCookie('frontendVersion', `http://${targetHost}/`, { domain, sameSite: 'lax' }); - cy.visit(targetUrlRaw, visitOptions); // Make sure the app has started loading before continuing cy.findByTestId('presentation').should('exist'); cy.injectAxe(); - - // Url suffix is used when logging in as another user in an existing instance. In those cases we might load assets - // multiple times, and that's OK. - if (!urlSuffix) { - // This guards against loading the app multiple times. This can happen if any of the login procedures end up - // visiting the app before we call cy.visit() above. In those cases the app might be loaded twice, and requests will - // be cancelled mid-flight, which may cause unexpected failures and lots of empty instances. The browser can also - // cache these requests, so they can be 0, which is also OK. - cy.get('@js.all').should('have.length.below', 2); - cy.get('@css.all').should('have.length.below', 2); - } }); export function getTargetUrl(appName: string) { @@ -159,15 +115,6 @@ export function getTargetUrl(appName: string) { : `https://ttd.apps.${Cypress.config('baseUrl')?.slice(8)}/ttd/${appName}`; } -function getConfiguredFrontendHost() { - const config = Cypress.config() as Cypress.ConfigOptions & { frontendUrl?: string }; - if (!config.frontendUrl) { - throw new Error('Missing Cypress frontendUrl config. Set frontendUrl, CYPRESS_HOST, or --env host=.'); - } - - return new URL(config.frontendUrl).host; -} - function generateHtmlToEval(javascript: string) { return ` diff --git a/src/Runtime/localtest/src/Controllers/HomeController.cs b/src/Runtime/localtest/src/Controllers/HomeController.cs index ae5f5f7d62e..0c6a5d626ea 100644 --- a/src/Runtime/localtest/src/Controllers/HomeController.cs +++ b/src/Runtime/localtest/src/Controllers/HomeController.cs @@ -87,13 +87,15 @@ public async Task Index([FromQuery(Name = "goto")] string goTo = { using var timeoutCancellationTokenSource = new CancellationTokenSource(LocalAppViewTimeout); var cancellationToken = timeoutCancellationTokenSource.Token; - model.TestApps = await GetAppsList(cancellationToken); + model.TestApps = await GetAppSelectionOptions(cancellationToken); model.TestUsers = await GetTestUsersAndPartiesSelectList(cancellationToken); model.UserSelect = Request.Cookies["Localtest_User.Party_Select"]; model.SelectRedirectApp(); - var selectedAppId = - model.AppPathSelection - ?? (model.TestApps.Count() == 1 ? model.TestApps.First().Value : null); + var selectedApp = + model.TestApps.FirstOrDefault(app => app.Selected) + ?? (model.TestApps.Count == 1 ? model.TestApps.First() : null); + var selectedAppId = model.AppPathSelection ?? selectedApp?.Value; + model.ShowFrontendVersionSwitcher = selectedApp?.ShowFrontendVersionSwitcher ?? true; var defaultAuthLevel = await GetAppAuthLevel(selectedAppId, cancellationToken); model.AuthenticationLevels = GetAuthenticationLevels(defaultAuthLevel); } @@ -521,6 +523,32 @@ private async Task> GetAppsList(CancellationToken cancellat return applications.Select((kv) => GetSelectItem(kv.Value, kv.Key)).ToList(); } + private async Task> GetAppSelectionOptions(CancellationToken cancellationToken = default) + { + var applications = await _localApp.GetApplications(cancellationToken); + var appOptions = new List(); + + foreach (var (path, app) in applications) + { + var appVersion = await _localApp.GetAppVersion(path, cancellationToken); + appOptions.Add( + new AppSelectionOption + { + Value = path, + Text = app.Id, + ShowFrontendVersionSwitcher = ShouldShowFrontendVersionSwitcher(appVersion), + } + ); + } + + return appOptions; + } + + private static bool ShouldShowFrontendVersionSwitcher(Version appVersion) + { + return appVersion is null || appVersion.Major < BrowserRoutingAppVersion.Major; + } + private static SelectListItem GetSelectItem(Application app, string path) { SelectListItem item = new SelectListItem() { Value = path, Text = app.Id }; diff --git a/src/Runtime/localtest/src/Models/StartAppModel.cs b/src/Runtime/localtest/src/Models/StartAppModel.cs index c9d44ed4d7d..ae014233e17 100644 --- a/src/Runtime/localtest/src/Models/StartAppModel.cs +++ b/src/Runtime/localtest/src/Models/StartAppModel.cs @@ -7,6 +7,17 @@ namespace LocalTest.Models { + public class AppSelectionOption + { + public string Value { get; set; } + + public string Text { get; set; } + + public bool Selected { get; set; } + + public bool ShowFrontendVersionSwitcher { get; set; } + } + public class StartAppModel { /// @@ -73,7 +84,12 @@ public class StartAppModel /// /// List of selectable Apps for dropdown /// - public List TestApps { get; set; } + public List TestApps { get; set; } + + /// + /// Whether frontend version switching UI should be visible for the selected app. + /// + public bool ShowFrontendVersionSwitcher { get; set; } = true; /// /// List of possible authentication levels @@ -98,6 +114,7 @@ public void SelectRedirectApp() selectedApp.Selected = true; AppPathSelection = selectedApp.Value; + ShowFrontendVersionSwitcher = selectedApp.ShowFrontendVersionSwitcher; } private string GetAppIdFromRedirectUrl() diff --git a/src/Runtime/localtest/src/Views/Home/Index.cshtml b/src/Runtime/localtest/src/Views/Home/Index.cshtml index 5ed7a8d708b..90f6d094fe6 100644 --- a/src/Runtime/localtest/src/Views/Home/Index.cshtml +++ b/src/Runtime/localtest/src/Views/Home/Index.cshtml @@ -65,7 +65,26 @@ {
    - @Html.DropDownListFor(model => model.AppPathSelection, Model.TestApps, new { Class = "form-control", id = "AppPathSelection" }) +
    }
    @@ -92,7 +111,7 @@ @if(!string.IsNullOrWhiteSpace(Model.LocalFrontendUrl)) { -