diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/api/package.json b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/api/package.json index 418c1a88185..7d180647c58 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/api/package.json +++ b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/api/package.json @@ -1,5 +1,6 @@ { "name": "api", + "version": "1.0.0", "private": true, "scripts": { "dev": "node server.js", diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/package-lock.json b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/package-lock.json index 21e8c7fc0fa..c68b8cad749 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/package-lock.json +++ b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "name": "nextjs", + "version": "1.0.0", "dependencies": { "next": "15.5.15", "react": "^19.0.0", diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/package.json b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/package.json index 326e830f271..dfb77eaad51 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/package.json +++ b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nextjs/package.json @@ -1,5 +1,6 @@ { "name": "nextjs", + "version": "1.0.0", "private": true, "scripts": { "dev": "next dev", diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nodeserver/package.json b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nodeserver/package.json index 1e7d689f980..287934fbf83 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nodeserver/package.json +++ b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/nodeserver/package.json @@ -1,5 +1,6 @@ { "name": "nodeserver", + "version": "1.0.0", "private": true, "scripts": { "dev": "node server.js", diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/npmscript/package-lock.json b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/npmscript/package-lock.json index cd50ac0bc93..3324c296e38 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/npmscript/package-lock.json +++ b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/npmscript/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "name": "npmscript", + "version": "1.0.0", "dependencies": { "express": "^4.21.0" } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/npmscript/package.json b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/npmscript/package.json index 466b3f76509..cc195e0ddaf 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/npmscript/package.json +++ b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/npmscript/package.json @@ -1,5 +1,6 @@ { "name": "npmscript", + "version": "1.0.0", "private": true, "scripts": { "dev": "node server.js", diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/staticsite/package.json b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/staticsite/package.json index afe8603493e..78663c6115b 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/staticsite/package.json +++ b/tests/Aspire.Cli.EndToEnd.Tests/Fixtures/JsPublish/staticsite/package.json @@ -1,5 +1,6 @@ { "name": "staticsite", + "version": "1.0.0", "private": true, "scripts": { "dev": "npx serve dist", diff --git a/tests/Aspire.Cli.EndToEnd.Tests/JavaScriptPublishTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/JavaScriptPublishTests.cs index 82972ac3c16..349444ea377 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/JavaScriptPublishTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/JavaScriptPublishTests.cs @@ -3,7 +3,6 @@ using Aspire.Cli.EndToEnd.Tests.Helpers; using Aspire.Cli.Tests.Utils; -using Aspire.TestUtilities; using Hex1b.Automation; using Xunit; @@ -22,12 +21,13 @@ public sealed class JavaScriptPublishTests(ITestOutputHelper output) [Fact] [CaptureWorkspaceOnFailure] - [QuarantinedTest("https://github.com/microsoft/aspire/issues/16188")] public async Task AllPublishMethodsBuildDockerImages() { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var strategy = CliInstallStrategy.Detect(output.WriteLine); using var workspace = TemporaryWorkspace.Create(output); + var localChannel = CliE2ETestHelpers.PrepareLocalChannel(repoRoot, strategy, + ["Aspire.Hosting.CodeGeneration.TypeScript.", "Aspire.Hosting.JavaScript.", "Aspire.Hosting.Docker."]); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); @@ -48,13 +48,18 @@ public async Task AllPublishMethodsBuildDockerImages() await auto.WaitUntilTextAsync("Created apphost.ts", timeout: TimeSpan.FromMinutes(2)); await auto.DeclineAgentInitPromptAsync(counter); + if (localChannel is not null) + { + CliE2ETestHelpers.WriteLocalChannelSettings(workspace.WorkspaceRoot.FullName, localChannel.SdkVersion); + } + await auto.TypeAsync("aspire add Aspire.Hosting.JavaScript"); await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(180)); + await auto.WaitForAspireAddCompletionAsync(counter, TimeSpan.FromSeconds(180)); await auto.TypeAsync("aspire add Aspire.Hosting.Docker"); await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(180)); + await auto.WaitForAspireAddCompletionAsync(counter, TimeSpan.FromSeconds(180)); // Copy checked-in fixture apps and write the apphost CopyFixtures(workspace); @@ -86,6 +91,89 @@ public async Task AllPublishMethodsBuildDockerImages() await pendingRun; } + [Fact] + [CaptureWorkspaceOnFailure] + public async Task JavaScriptHostingApisRunFromTypeScriptAppHost() + { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var strategy = CliInstallStrategy.Detect(output.WriteLine); + using var workspace = TemporaryWorkspace.Create(output); + var localChannel = CliE2ETestHelpers.PrepareLocalChannel(repoRoot, strategy, + ["Aspire.Hosting.CodeGeneration.TypeScript.", "Aspire.Hosting.JavaScript."]); + + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, workspace: workspace); + + var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); + var counter = new SequenceCounter(); + var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + var testBodyFailed = false; + + try + { + await auto.PrepareDockerEnvironmentAsync(counter, workspace); + await auto.InstallAspireCliAsync(strategy, counter); + + await auto.RunCommandFailFastAsync("aspire init --language typescript --non-interactive", counter, TimeSpan.FromMinutes(2)); + + if (localChannel is not null) + { + CliE2ETestHelpers.WriteLocalChannelSettings(workspace.WorkspaceRoot.FullName, localChannel.SdkVersion); + } + + await auto.TypeAsync("aspire add Aspire.Hosting.JavaScript"); + await auto.EnterAsync(); + await auto.WaitForAspireAddCompletionAsync(counter, TimeSpan.FromMinutes(2)); + + WriteRuntimeFixtures(workspace); + WriteRuntimeAppHost(workspace); + WriteRuntimeVerificationScript(workspace); + + await auto.RunCommandFailFastAsync("unset ASPIRE_PLAYGROUND", counter); + + await auto.RunCommandFailFastAsync("aspire run > aspire-run.log 2>&1 & echo $! > aspire-run.pid", counter); + await auto.RunCommandFailFastAsync("bash verify-runtime.sh", counter, TimeSpan.FromMinutes(2)); + } + catch + { + testBodyFailed = true; + throw; + } + finally + { + try + { + await auto.RunCommandAsync("if [ -f aspire-run.pid ]; then kill \"$(cat aspire-run.pid)\" 2>/dev/null || true; wait \"$(cat aspire-run.pid)\" 2>/dev/null || true; fi", counter, TimeSpan.FromMinutes(1)); + } + catch + { + // Best effort. A failure before aspire run writes its PID leaves no process to stop. + } + + try + { + await auto.CaptureAspireDiagnosticsAsync(counter, workspace); + } + catch + { + // Best effort diagnostics capture. + } + + try + { + await auto.TypeAsync("exit"); + await auto.EnterAsync(); + await pendingRun; + } + catch + { + if (!testBodyFailed) + { + throw; + } + } + } + } + private static void WriteAppHost(TemporaryWorkspace workspace) { var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts"); @@ -100,6 +188,7 @@ private static void WriteAppHost(TemporaryWorkspace workspace) .withExternalHttpEndpoints(); await builder.addViteApp('staticsite', './staticsite') + .withHttpEndpoint({ name: 'http', targetPort: 5000 }) .publishAsStaticWebsite({ apiPath: '/api', apiTarget: api }) .withExternalHttpEndpoints(); @@ -118,6 +207,155 @@ await builder.addNextJsApp('nextjs', './nextjs') """); } + private static void WriteRuntimeAppHost(TemporaryWorkspace workspace) + { + var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts"); + File.WriteAllText(appHostPath, $$""" + import { createBuilder } from './.modules/aspire.js'; + + const builder = await createBuilder(); + + const nodeNpm = await builder.addNodeApp('node-npm', './node-npm', 'server.js'); + await nodeNpm.withNpm({ install: false }); + await nodeNpm.withRunScript('start'); + await nodeNpm.withHttpEndpoint({ name: 'http', env: 'PORT' }); + + const javaScriptPnpm = await builder.addJavaScriptApp('javascript-pnpm', './javascript-pnpm', { runScriptName: 'dev' }); + await javaScriptPnpm.withPnpm({ install: false }); + + const viteYarn = await builder.addViteApp('vite-yarn', './vite-yarn', { runScriptName: 'dev' }); + await viteYarn.withYarn({ install: false }); + + const nextBun = await builder.addNextJsApp('next-bun', './next-bun', { runScriptName: 'dev' }); + await nextBun.disableBuildValidation(); + await nextBun.withBun({ install: false }); + + await builder.build().run(); + """); + } + + private static void WriteRuntimeFixtures(TemporaryWorkspace workspace) + { + WriteRuntimeApp(workspace, "node-npm", "start"); + WriteRuntimeApp(workspace, "javascript-pnpm", "dev"); + WriteRuntimeApp(workspace, "vite-yarn", "dev", packageManager: "yarn@4.14.1"); + WriteRuntimeApp(workspace, "next-bun", "dev"); + } + + private static void WriteRuntimeApp(TemporaryWorkspace workspace, string appName, string scriptName, string? packageManager = null) + { + var appDir = Path.Combine(workspace.WorkspaceRoot.FullName, appName); + Directory.CreateDirectory(appDir); + + var packageManagerProperty = packageManager is not null ? $""" + "packageManager": "{packageManager}", + """ : string.Empty; + + File.WriteAllText(Path.Combine(appDir, "package.json"), $$""" + { + "name": "{{appName}}", + "private": true, + {{packageManagerProperty}} + "scripts": { + "{{scriptName}}": "node server.js" + } + } + """); + + if (packageManager?.StartsWith("yarn@", StringComparison.OrdinalIgnoreCase) == true) + { + // Yarn 2+ requires a project-local lockfile with the workspace package entry before + // it will run scripts, even when Aspire is configured not to run package install. + File.WriteAllText(Path.Combine(appDir, "yarn.lock"), $$""" + # This file is generated by running "yarn install" inside your project. + # Manual changes might be lost - proceed with caution! + + __metadata: + version: 9 + cacheKey: 10c0 + + "{{appName}}@workspace:.": + version: 0.0.0-use.local + resolution: "{{appName}}@workspace:." + languageName: unknown + linkType: soft + + """); + } + + File.WriteAllText(Path.Combine(appDir, "server.js"), $$""" + const http = require('http'); + const fs = require('fs'); + + const portArgumentIndex = process.argv.findIndex(arg => arg === '--port' || arg === '-p'); + const fallbackPorts = { + 'node-npm': 3001, + 'javascript-pnpm': 3002, + 'vite-yarn': 3003, + 'next-bun': 3004, + }; + const port = process.env.PORT || (portArgumentIndex >= 0 ? process.argv[portArgumentIndex + 1] : undefined) || fallbackPorts['{{appName}}']; + + http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ app: '{{appName}}', ok: true })); + }).listen(port, '0.0.0.0', () => { + fs.writeFileSync('ready-port', port.toString()); + console.log('{{appName}} listening on ' + port); + }); + """); + } + + private static void WriteRuntimeVerificationScript(TemporaryWorkspace workspace) + { + File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "verify-runtime.sh"), $$""" + #!/usr/bin/env bash + set -euo pipefail + + check_endpoint() { + local name="$1" + local port_file="${name}/ready-port" + + for i in $(seq 1 30); do + if [ ! -f "${port_file}" ]; then + sleep 1 + continue + fi + + local port + port="$(cat "${port_file}")" + if curl -sf --max-time 5 "http://localhost:${port}/" | grep -q "\"app\":\"${name}\""; then + echo "${name}_OK" + return 0 + fi + + sleep 1 + done + + echo "${name}_FAIL" + echo "===== aspire-run.log =====" + cat aspire-run.log || true + echo "===== end aspire-run.log =====" + echo "===== runtime fixture files =====" + for file in package.json node-npm/package.json node-npm/ready-port javascript-pnpm/package.json javascript-pnpm/ready-port vite-yarn/package.json vite-yarn/yarn.lock vite-yarn/ready-port next-bun/package.json next-bun/ready-port; do + if [ -f "$file" ]; then + echo "--- $file" + cat "$file" + fi + done + echo "===== end runtime fixture files =====" + return 1 + } + + check_endpoint "node-npm" + check_endpoint "javascript-pnpm" + check_endpoint "vite-yarn" + check_endpoint "next-bun" + + echo "RUNTIME_ALL_OK" + """); + } + private static void CopyFixtures(TemporaryWorkspace workspace) { // Copy root-level files (e.g. verify.sh) diff --git a/tests/Aspire.Deployment.EndToEnd.Tests/TypeScriptJavaScriptHostingDeploymentTests.cs b/tests/Aspire.Deployment.EndToEnd.Tests/TypeScriptJavaScriptHostingDeploymentTests.cs new file mode 100644 index 00000000000..873272ad8b5 --- /dev/null +++ b/tests/Aspire.Deployment.EndToEnd.Tests/TypeScriptJavaScriptHostingDeploymentTests.cs @@ -0,0 +1,224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Tests.Utils; +using Aspire.Deployment.EndToEnd.Tests.Helpers; +using Hex1b.Automation; +using Xunit; + +namespace Aspire.Deployment.EndToEnd.Tests; + +/// +/// End-to-end tests for deploying TypeScript AppHosts that use Aspire.Hosting.JavaScript publish APIs. +/// +public sealed class TypeScriptJavaScriptHostingDeploymentTests(ITestOutputHelper output) +{ + private static readonly TimeSpan s_testTimeout = TimeSpan.FromMinutes(40); + + [Fact] + public async Task DeployTypeScriptStaticWebsiteWithNodeApiToAzureContainerApps() + { + using var cts = new CancellationTokenSource(s_testTimeout); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( + cts.Token, TestContext.Current.CancellationToken); + var cancellationToken = linkedCts.Token; + + await DeployTypeScriptStaticWebsiteWithNodeApiToAzureContainerAppsCore(cancellationToken); + } + + private async Task DeployTypeScriptStaticWebsiteWithNodeApiToAzureContainerAppsCore(CancellationToken cancellationToken) + { + var subscriptionId = AzureAuthenticationHelpers.TryGetSubscriptionId(); + if (string.IsNullOrEmpty(subscriptionId)) + { + Assert.Skip("Azure subscription not configured. Set ASPIRE_DEPLOYMENT_TEST_SUBSCRIPTION."); + } + + if (!AzureAuthenticationHelpers.IsAzureAuthAvailable()) + { + if (DeploymentE2ETestHelpers.IsRunningInCI) + { + Assert.Fail("Azure authentication not available in CI. Check OIDC configuration."); + } + + Assert.Skip("Azure authentication not available. Run 'az login' to authenticate."); + } + + using var workspace = TemporaryWorkspace.Create(output); + var startTime = DateTime.UtcNow; + var resourceGroupName = DeploymentE2ETestHelpers.GenerateResourceGroupName("ts-js-hosting"); + + output.WriteLine($"Test: {nameof(DeployTypeScriptStaticWebsiteWithNodeApiToAzureContainerApps)}"); + output.WriteLine($"Resource Group: {resourceGroupName}"); + output.WriteLine($"Subscription: {subscriptionId[..8]}..."); + output.WriteLine($"Workspace: {workspace.WorkspaceRoot.FullName}"); + + try + { + using var terminal = DeploymentE2ETestHelpers.CreateTestTerminal(); + var pendingRun = terminal.RunAsync(cancellationToken); + + var counter = new SequenceCounter(); + var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + + await auto.PrepareEnvironmentAsync(workspace, counter); + await auto.InstallCurrentBuildAspireBundleAsync(counter, output); + + await auto.RunCommandFailFastAsync("aspire init --language typescript --non-interactive", counter, TimeSpan.FromMinutes(2)); + + await AddPackageAsync(auto, counter, "Aspire.Hosting.JavaScript"); + await AddPackageAsync(auto, counter, "Aspire.Hosting.Azure.AppContainers"); + + WriteStaticWebsiteWithNodeApiAppHost(workspace); + + await auto.RunCommandFailFastAsync($"unset ASPIRE_PLAYGROUND && export AZURE__LOCATION=westus3 && export AZURE__RESOURCEGROUP={resourceGroupName}", counter); + + await auto.TypeAsync("aspire deploy --clear-cache"); + await auto.EnterAsync(); + await auto.WaitForPipelineSuccessAsync(timeout: TimeSpan.FromMinutes(30)); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); + + await auto.RunCommandFailFastAsync(BuildEndpointVerificationCommand(resourceGroupName), counter, TimeSpan.FromMinutes(10)); + + await auto.TypeAsync("exit"); + await auto.EnterAsync(); + await pendingRun; + + var duration = DateTime.UtcNow - startTime; + DeploymentReporter.ReportDeploymentSuccess( + nameof(DeployTypeScriptStaticWebsiteWithNodeApiToAzureContainerApps), + resourceGroupName, + new Dictionary(), + duration); + } + catch (Exception ex) + { + var duration = DateTime.UtcNow - startTime; + output.WriteLine($"Test failed after {duration}: {ex.Message}"); + + DeploymentReporter.ReportDeploymentFailure( + nameof(DeployTypeScriptStaticWebsiteWithNodeApiToAzureContainerApps), + resourceGroupName, + ex.Message, + ex.StackTrace); + + throw; + } + finally + { + output.WriteLine($"Triggering cleanup of resource group: {resourceGroupName}"); + TriggerCleanupResourceGroup(resourceGroupName, output); + DeploymentReporter.ReportCleanupStatus(resourceGroupName, success: true, errorMessage: "Cleanup triggered (fire-and-forget)"); + } + } + + private static async Task AddPackageAsync(Hex1bTerminalAutomator auto, SequenceCounter counter, string packageName) + { + await auto.TypeAsync($"aspire add {packageName}"); + await auto.EnterAsync(); + await auto.WaitForAspireAddCompletionAsync(counter, TimeSpan.FromMinutes(3)); + } + + private static void WriteStaticWebsiteWithNodeApiAppHost(TemporaryWorkspace workspace) + { + var apiDir = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "api")); + File.WriteAllText(Path.Combine(apiDir.FullName, "package.json"), """ + { + "name": "api", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "echo 'no build needed'" + } + } + """); + File.WriteAllText(Path.Combine(apiDir.FullName, "server.js"), """ + const http = require('http'); + const port = process.env.PORT || 3000; + + http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify([{ temperatureC: 22, summary: 'Warm' }])); + }).listen(port, '0.0.0.0'); + """); + + var staticSiteDir = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "staticsite")); + File.WriteAllText(Path.Combine(staticSiteDir.FullName, "package.json"), """ + { + "name": "staticsite", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "mkdir -p dist && cp index.html dist/index.html" + } + } + """); + File.WriteAllText(Path.Combine(staticSiteDir.FullName, "index.html"), """ + + + Static Site +

Weather

+ + """); + + File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts"), """ + import { createBuilder } from './.modules/aspire.js'; + + const builder = await createBuilder(); + + // This environment selects Azure Container Apps as the deployment target for these app resources. + await builder.addAzureContainerAppEnvironment('env'); + + const api = await builder.addNodeApp('api', './api', 'server.js') + .withHttpEndpoint({ name: 'http', env: 'PORT' }); + + await builder.addJavaScriptApp('staticsite', './staticsite') + .withHttpEndpoint({ name: 'http', targetPort: 5000 }) + .publishAsStaticWebsite({ apiPath: '/api', apiTarget: api }) + .withExternalHttpEndpoints(); + + await builder.build().run(); + """); + } + + private static string BuildEndpointVerificationCommand(string resourceGroupName) + { + return + $"static_host=$(az containerapp list -g \"{resourceGroupName}\" --query \"[?contains(name, 'staticsite') && properties.configuration.ingress.external == \\`true\\`].properties.configuration.ingress.fqdn | [0]\" -o tsv) && " + + $"if [ -z \"$static_host\" ]; then echo \"No external staticsite endpoint found\"; az containerapp list -g \"{resourceGroupName}\" --query \"[].{{name:name,external:properties.configuration.ingress.external,fqdn:properties.configuration.ingress.fqdn}}\" -o table; exit 1; fi && " + + "echo \"Checking https://$static_host\" && " + + "ok=0 && " + + "for i in $(seq 1 30); do " + + "if curl -sf --max-time 5 \"https://$static_host/index.html\" | grep -q Weather && " + + "curl -sf --max-time 5 \"https://$static_host/api/weather\" | grep -q temperatureC; then " + + "ok=1; break; " + + "fi; " + + "sleep 5; " + + "done && " + + "if [ \"$ok\" -ne 1 ]; then echo \"Endpoint verification failed\"; exit 1; fi"; + } + + private static void TriggerCleanupResourceGroup(string resourceGroupName, ITestOutputHelper output) + { + using var process = new System.Diagnostics.Process + { + StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "az", + Arguments = $"group delete --name {resourceGroupName} --yes --no-wait", + UseShellExecute = false, + CreateNoWindow = true + } + }; + + try + { + process.Start(); + output.WriteLine($"Cleanup triggered for resource group: {resourceGroupName}"); + } + catch (Exception ex) + { + output.WriteLine($"Failed to trigger cleanup: {ex.Message}"); + } + } +} diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.JavaScript/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.JavaScript/TypeScript/apphost.ts index b6604f990b0..47b693ea62f 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.JavaScript/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.JavaScript/TypeScript/apphost.ts @@ -28,4 +28,26 @@ const _viteAppName = await viteApp.name(); const _viteAppCommand = await viteApp.command(); const _viteAppWorkingDirectory = await viteApp.workingDirectory(); +const nextJsApp = await builder.addNextJsApp('nextjs-app', './nextjs-app', { runScriptName: 'dev' }); +await nextJsApp.disableBuildValidation(); +await nextJsApp.withNpm({ install: false, installCommand: 'ci' }); +const _nextJsAppName = await nextJsApp.name(); +const _nextJsAppCommand = await nextJsApp.command(); +const _nextJsAppWorkingDirectory = await nextJsApp.workingDirectory(); + +const staticSiteApp = await builder.addJavaScriptApp('static-site-app', './static-site-app'); +await staticSiteApp.publishAsStaticWebsite({ + apiPath: '/api', + apiTarget: nodeApp, + outputPath: 'dist', + stripPrefix: true, + targetEndpointName: 'http', +}); + +await builder.addJavaScriptApp('node-server-app', './node-server-app') + .publishAsNodeServer('server.js', { outputPath: 'build' }); + +await builder.addJavaScriptApp('npm-script-app', './npm-script-app') + .publishAsNpmScript({ startScriptName: 'start', runScriptArguments: '-- --port $PORT' }); + await builder.build().run();