diff --git a/playwright/Customizations/AAP.spec.ts b/playwright/Customizations/AAP.spec.ts index b4a9fecbf4..c4325cfc60 100644 --- a/playwright/Customizations/AAP.spec.ts +++ b/playwright/Customizations/AAP.spec.ts @@ -1,3 +1,6 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; @@ -183,15 +186,20 @@ test('Create a blueprint with AAP registration customization', async ({ .getByRole('button', { name: 'Save changes to blueprint' }) .click(); }); + + let exportedBP = ''; // This is for hosted service only as these features are not available in cockpit plugin await test.step('Export BP', async (step) => { step.skip(!isHosted(), 'Exporting is not available in the plugin'); - await exportBlueprint(page, blueprintName); + exportedBP = await exportBlueprint(page); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(exportedBP), { recursive: true }); + }); }); await test.step('Import BP', async (step) => { step.skip(!isHosted(), 'Importing is not available in the plugin'); - await importBlueprint(page, blueprintName); + await importBlueprint(page, exportedBP); }); await test.step('Review imported BP', async (step) => { diff --git a/playwright/Customizations/Filesystem.spec.ts b/playwright/Customizations/Filesystem.spec.ts index 33cb46cb18..e354f11582 100644 --- a/playwright/Customizations/Filesystem.spec.ts +++ b/playwright/Customizations/Filesystem.spec.ts @@ -1,8 +1,12 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; import { FILE_SYSTEM_CUSTOMIZATION_URL } from '../../src/constants'; import { test } from '../fixtures/cleanup'; +import { exportedFilesystemBP } from '../fixtures/data/exportBlueprintContents'; import { isHosted } from '../helpers/helpers'; import { ensureAuthenticated } from '../helpers/login'; import { @@ -18,6 +22,7 @@ import { fillInImageOutputGuest, importBlueprint, registerLater, + verifyExportedBlueprint, } from '../helpers/wizardHelpers'; test('Create a blueprint with Filesystem customization', async ({ @@ -178,15 +183,28 @@ test('Create a blueprint with Filesystem customization', async ({ .click(); }); - // This is for hosted service only as these features are not available in cockpit plugin - await test.step('Export BP', async (step) => { - step.skip(!isHosted(), 'Exporting is not available in the plugin'); - await exportBlueprint(page, blueprintName); + let exportedBP = ''; + await test.step('Export BP', async () => { + exportedBP = await exportBlueprint(page); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(exportedBP), { recursive: true }); + }); + }); + + await test.step('Review exported BP', async (step) => { + step.skip( + isHosted(), + 'Only verify the contents of the exported blueprint in cockpit', + ); + await verifyExportedBlueprint( + exportedBP, + exportedFilesystemBP(blueprintName), + ); }); await test.step('Import BP', async (step) => { step.skip(!isHosted(), 'Importing is not available in the plugin'); - await importBlueprint(page, blueprintName); + await importBlueprint(page, exportedBP); }); await test.step('Review imported BP', async (step) => { diff --git a/playwright/Customizations/Firewall.spec.ts b/playwright/Customizations/Firewall.spec.ts index fb33914f81..9fcfbd65d7 100644 --- a/playwright/Customizations/Firewall.spec.ts +++ b/playwright/Customizations/Firewall.spec.ts @@ -1,7 +1,11 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; import { test } from '../fixtures/customizations'; +import { exportedFirewallBP } from '../fixtures/data/exportBlueprintContents'; import { isHosted } from '../helpers/helpers'; import { ensureAuthenticated } from '../helpers/login'; import { @@ -17,6 +21,7 @@ import { fillInImageOutputGuest, importBlueprint, registerLater, + verifyExportedBlueprint, } from '../helpers/wizardHelpers'; test('Create a blueprint with Firewall customization', async ({ @@ -194,15 +199,28 @@ test('Create a blueprint with Firewall customization', async ({ .click(); }); - // This is for hosted service only as these features are not available in cockpit plugin - await test.step('Export BP', async (step) => { - step.skip(!isHosted(), 'Exporting is not available in the plugin'); - await exportBlueprint(page, blueprintName); + let exportedBP = ''; + await test.step('Export BP', async () => { + exportedBP = await exportBlueprint(page); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(exportedBP), { recursive: true }); + }); + }); + + await test.step('Review exported BP', async (step) => { + step.skip( + isHosted(), + 'Only verify the contents of the exported blueprint in cockpit', + ); + await verifyExportedBlueprint( + exportedBP, + exportedFirewallBP(blueprintName), + ); }); await test.step('Import BP', async (step) => { step.skip(!isHosted(), 'Importing is not available in the plugin'); - await importBlueprint(page, blueprintName); + await importBlueprint(page, exportedBP); }); await test.step('Review imported BP', async (step) => { diff --git a/playwright/Customizations/Hostname.spec.ts b/playwright/Customizations/Hostname.spec.ts index 948d46a8ec..87497e0a37 100644 --- a/playwright/Customizations/Hostname.spec.ts +++ b/playwright/Customizations/Hostname.spec.ts @@ -1,7 +1,11 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; import { test } from '../fixtures/customizations'; +import { exportedHostnameBP } from '../fixtures/data/exportBlueprintContents'; import { isHosted } from '../helpers/helpers'; import { ensureAuthenticated } from '../helpers/login'; import { @@ -17,6 +21,7 @@ import { fillInImageOutputGuest, importBlueprint, registerLater, + verifyExportedBlueprint, } from '../helpers/wizardHelpers'; test('Create a blueprint with Hostname customization', async ({ @@ -67,15 +72,28 @@ test('Create a blueprint with Hostname customization', async ({ .click(); }); - // This is for hosted service only as these features are not available in cockpit plugin - await test.step('Export BP', async (step) => { - step.skip(!isHosted(), 'Exporting is not available in the plugin'); - await exportBlueprint(page, blueprintName); + let exportedBP = ''; + await test.step('Export BP', async () => { + exportedBP = await exportBlueprint(page); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(exportedBP), { recursive: true }); + }); + }); + + await test.step('Review exported BP', async (step) => { + step.skip( + isHosted(), + 'Only verify the contents of the exported blueprint in cockpit', + ); + await verifyExportedBlueprint( + exportedBP, + exportedHostnameBP(blueprintName), + ); }); await test.step('Import BP', async (step) => { step.skip(!isHosted(), 'Importing is not available in the plugin'); - await importBlueprint(page, blueprintName); + await importBlueprint(page, exportedBP); }); await test.step('Review imported BP', async (step) => { diff --git a/playwright/Customizations/Kernel.spec.ts b/playwright/Customizations/Kernel.spec.ts index d21444e543..e913ae73f9 100644 --- a/playwright/Customizations/Kernel.spec.ts +++ b/playwright/Customizations/Kernel.spec.ts @@ -1,7 +1,11 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; import { test } from '../fixtures/customizations'; +import { exportedKernelBP } from '../fixtures/data/exportBlueprintContents'; import { isHosted } from '../helpers/helpers'; import { ensureAuthenticated } from '../helpers/login'; import { @@ -17,6 +21,7 @@ import { fillInImageOutputGuest, importBlueprint, registerLater, + verifyExportedBlueprint, } from '../helpers/wizardHelpers'; test('Create a blueprint with Kernel customization', async ({ @@ -105,15 +110,25 @@ test('Create a blueprint with Kernel customization', async ({ .click(); }); - // This is for hosted service only as these features are not available in cockpit plugin - await test.step('Export BP', async (step) => { - step.skip(!isHosted(), 'Exporting is not available in the plugin'); - await exportBlueprint(page, blueprintName); + let exportedBP = ''; + await test.step('Export BP', async () => { + exportedBP = await exportBlueprint(page); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(exportedBP), { recursive: true }); + }); + }); + + await test.step('Review exported BP', async (step) => { + step.skip( + isHosted(), + 'Only verify the contents of the exported blueprint in cockpit', + ); + await verifyExportedBlueprint(exportedBP, exportedKernelBP(blueprintName)); }); await test.step('Import BP', async (step) => { step.skip(!isHosted(), 'Importing is not available in the plugin'); - await importBlueprint(page, blueprintName); + await importBlueprint(page, exportedBP); }); await test.step('Review imported BP', async (step) => { diff --git a/playwright/Customizations/Locale.spec.ts b/playwright/Customizations/Locale.spec.ts index a9d4a656e6..7fa1ff5b17 100644 --- a/playwright/Customizations/Locale.spec.ts +++ b/playwright/Customizations/Locale.spec.ts @@ -1,7 +1,11 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; import { test } from '../fixtures/customizations'; +import { exportedLocaleBP } from '../fixtures/data/exportBlueprintContents'; import { isHosted } from '../helpers/helpers'; import { ensureAuthenticated } from '../helpers/login'; import { @@ -17,6 +21,7 @@ import { fillInImageOutputGuest, importBlueprint, registerLater, + verifyExportedBlueprint, } from '../helpers/wizardHelpers'; test('Create a blueprint with Locale customization', async ({ @@ -125,15 +130,25 @@ test('Create a blueprint with Locale customization', async ({ .click(); }); - // This is for hosted service only as these features are not available in cockpit plugin - await test.step('Export BP', async (step) => { - step.skip(!isHosted(), 'Exporting is not available in the plugin'); - await exportBlueprint(page, blueprintName); + let exportedBP = ''; + await test.step('Export BP', async () => { + exportedBP = await exportBlueprint(page); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(exportedBP), { recursive: true }); + }); + }); + + await test.step('Review exported BP', async (step) => { + step.skip( + isHosted(), + 'Only verify the contents of the exported blueprint in cockpit', + ); + await verifyExportedBlueprint(exportedBP, exportedLocaleBP(blueprintName)); }); await test.step('Import BP', async (step) => { step.skip(!isHosted(), 'Importing is not available in the plugin'); - await importBlueprint(page, blueprintName); + await importBlueprint(page, exportedBP); }); await test.step('Review imported BP', async (step) => { diff --git a/playwright/Customizations/OpenSCAP.spec.ts b/playwright/Customizations/OpenSCAP.spec.ts index 5aabacf246..188a8fc8ec 100644 --- a/playwright/Customizations/OpenSCAP.spec.ts +++ b/playwright/Customizations/OpenSCAP.spec.ts @@ -1,3 +1,6 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; @@ -228,13 +231,16 @@ test('Create a blueprint with OpenSCAP customization', async ({ .click(); }); - // This is for hosted service only as these features are not available in cockpit plugin + let exportedBP = ''; await test.step('Export BP', async () => { - await exportBlueprint(page, blueprintName); + exportedBP = await exportBlueprint(page); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(exportedBP), { recursive: true }); + }); }); await test.step('Import BP', async () => { - await importBlueprint(page, blueprintName); + await importBlueprint(page, exportedBP); }); await test.step('Review imported BP', async () => { diff --git a/playwright/Customizations/Registration.spec.ts b/playwright/Customizations/Registration.spec.ts index 27c2f39f20..c6fada5a36 100644 --- a/playwright/Customizations/Registration.spec.ts +++ b/playwright/Customizations/Registration.spec.ts @@ -1,3 +1,6 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; @@ -353,15 +356,17 @@ registrationModes.forEach( .click(); }); - // This is for hosted service only as these features are not available in cockpit plugin - await test.step('Export blueprint', async (step) => { - step.skip(!isHosted(), 'Exporting is not available in the plugin'); - await exportBlueprint(page, blueprintName); + let exportedBP = ''; + await test.step('Export blueprint', async () => { + exportedBP = await exportBlueprint(page); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(exportedBP), { recursive: true }); + }); }); await test.step('Import blueprint', async (step) => { step.skip(!isHosted(), 'Importing is not available in the plugin'); - await importBlueprint(page, blueprintName); + await importBlueprint(page, exportedBP); }); await test.step('Verify import does not change registration settings', async (step) => { diff --git a/playwright/Customizations/Systemd.spec.ts b/playwright/Customizations/Systemd.spec.ts index 14aa058516..739bb55605 100644 --- a/playwright/Customizations/Systemd.spec.ts +++ b/playwright/Customizations/Systemd.spec.ts @@ -1,7 +1,11 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; import { test } from '../fixtures/customizations'; +import { exportedSystemdBP } from '../fixtures/data/exportBlueprintContents'; import { isHosted } from '../helpers/helpers'; import { ensureAuthenticated } from '../helpers/login'; import { @@ -17,6 +21,7 @@ import { fillInImageOutputGuest, importBlueprint, registerLater, + verifyExportedBlueprint, } from '../helpers/wizardHelpers'; test('Create a blueprint with Systemd customization', async ({ @@ -127,15 +132,25 @@ test('Create a blueprint with Systemd customization', async ({ .click(); }); - // This is for hosted service only as these features are not available in cockpit plugin - await test.step('Export BP', async (step) => { - step.skip(!isHosted(), 'Exporting is not available in the plugin'); - await exportBlueprint(page, blueprintName); + let exportedBP = ''; + await test.step('Export BP', async () => { + exportedBP = await exportBlueprint(page); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(exportedBP), { recursive: true }); + }); + }); + + await test.step('Review exported BP', async (step) => { + step.skip( + isHosted(), + 'Only verify the contents of the exported blueprint in cockpit', + ); + await verifyExportedBlueprint(exportedBP, exportedSystemdBP(blueprintName)); }); await test.step('Import BP', async (step) => { step.skip(!isHosted(), 'Importing is not available in the plugin'); - await importBlueprint(page, blueprintName); + await importBlueprint(page, exportedBP); }); await test.step('Review imported BP', async (step) => { diff --git a/playwright/Customizations/Timezone.spec.ts b/playwright/Customizations/Timezone.spec.ts index 728a185041..064e0896f0 100644 --- a/playwright/Customizations/Timezone.spec.ts +++ b/playwright/Customizations/Timezone.spec.ts @@ -1,7 +1,11 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; import { test } from '../fixtures/customizations'; +import { exportedTimezoneBP } from '../fixtures/data/exportBlueprintContents'; import { isHosted } from '../helpers/helpers'; import { ensureAuthenticated } from '../helpers/login'; import { @@ -17,6 +21,7 @@ import { fillInImageOutputGuest, importBlueprint, registerLater, + verifyExportedBlueprint, } from '../helpers/wizardHelpers'; test('Create a blueprint with Timezone customization', async ({ @@ -102,15 +107,28 @@ test('Create a blueprint with Timezone customization', async ({ .click(); }); - // This is for hosted service only as these features are not available in cockpit plugin - await test.step('Export BP', async (step) => { - step.skip(!isHosted(), 'Exporting is not available in the plugin'); - await exportBlueprint(page, blueprintName); + let exportedBP = ''; + await test.step('Export BP', async () => { + exportedBP = await exportBlueprint(page); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(exportedBP), { recursive: true }); + }); + }); + + await test.step('Review exported BP', async (step) => { + step.skip( + isHosted(), + 'Only verify the contents of the exported blueprint in cockpit', + ); + await verifyExportedBlueprint( + exportedBP, + exportedTimezoneBP(blueprintName), + ); }); await test.step('Import BP', async (step) => { step.skip(!isHosted(), 'Importing is not available in the plugin'); - await importBlueprint(page, blueprintName); + await importBlueprint(page, exportedBP); }); await test.step('Review imported BP', async (step) => { diff --git a/playwright/Customizations/Users.spec.ts b/playwright/Customizations/Users.spec.ts index 282853c221..e9d2ea4789 100644 --- a/playwright/Customizations/Users.spec.ts +++ b/playwright/Customizations/Users.spec.ts @@ -1,7 +1,11 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; import { test } from '../fixtures/customizations'; +import { exportedUsersBP } from '../fixtures/data/exportBlueprintContents'; import { isHosted } from '../helpers/helpers'; import { ensureAuthenticated } from '../helpers/login'; import { @@ -17,6 +21,7 @@ import { fillInImageOutputGuest, importBlueprint, registerLater, + verifyExportedBlueprint, } from '../helpers/wizardHelpers'; const validRSAKey = @@ -413,15 +418,25 @@ test('Create a blueprint with Users customization', async ({ .click(); }); - // This is for hosted service only as these features are not available in cockpit plugin - await test.step('Export blueprint', async (step) => { - step.skip(!isHosted(), 'Exporting is not available in the plugin'); - await exportBlueprint(page, blueprintName); + let exportedBP = ''; + await test.step('Export BP', async () => { + exportedBP = await exportBlueprint(page); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(exportedBP), { recursive: true }); + }); + }); + + await test.step('Review exported BP', async (step) => { + step.skip( + isHosted(), + 'Only verify the contents of the exported blueprint in cockpit', + ); + await verifyExportedBlueprint(exportedBP, exportedUsersBP(blueprintName)); }); await test.step('Import blueprint', async (step) => { step.skip(!isHosted(), 'Importing is not available in the plugin'); - await importBlueprint(page, blueprintName); + await importBlueprint(page, exportedBP); }); await test.step('Verify imported users', async (step) => { diff --git a/playwright/Import/Import.spec.ts b/playwright/Import/Import.spec.ts index bef6910595..11fc506e30 100644 --- a/playwright/Import/Import.spec.ts +++ b/playwright/Import/Import.spec.ts @@ -1,3 +1,6 @@ +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; + import { expect } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; @@ -32,11 +35,13 @@ test('Import a blueprint with invalid customization', async ({ const frame = await ibFrame(page); await test.step('Import BP', async () => { - await saveBlueprintFileWithContents( - blueprintName, + const blueprintFile = await saveBlueprintFileWithContents( IMPORT_WITH_DUPLICATE_VALUES, ); - await importBlueprint(page, blueprintName); + await cleanup.add(async () => { + await fsPromises.rm(path.dirname(blueprintFile), { recursive: true }); + }); + await importBlueprint(page, blueprintFile); }); await test.step('Navigate to optional steps in Wizard', async () => { diff --git a/playwright/fixtures/data/exportBlueprintContents.ts b/playwright/fixtures/data/exportBlueprintContents.ts new file mode 100644 index 0000000000..cc499eafb9 --- /dev/null +++ b/playwright/fixtures/data/exportBlueprintContents.ts @@ -0,0 +1,139 @@ +export const exportedFilesystemBP = (blueprintName: string): string => { + return `name = "${blueprintName}" + +[customizations] +[[customizations.filesystem]] +mountpoint = "/" +minsize = 10737418240 + +[[customizations.filesystem]] +mountpoint = "/srv/data" +minsize = 1073741824 + +[customizations.timezone] +timezone = "Etc/UTC" + +[customizations.locale] +languages = [ "C.UTF-8" ]`; +}; + +export const exportedFirewallBP = (blueprintName: string): string => { + return `name = "${blueprintName}" + +[customizations] +[customizations.timezone] +timezone = "Etc/UTC" + +[customizations.locale] +languages = [ "C.UTF-8" ] + +[customizations.firewall] +ports = [ "90:tcp" ] + +[customizations.firewall.services] +enabled = [ "x" ] +disabled = [ "y" ]`; +}; + +export const exportedHostnameBP = (blueprintName: string): string => { + return `name = "${blueprintName}" + +[customizations] +hostname = "testsystemedited" + +[customizations.timezone] +timezone = "Etc/UTC" + +[customizations.locale] +languages = [ "C.UTF-8" ]`; +}; + +export const exportedKernelBP = (blueprintName: string): string => { + return `name = "${blueprintName}" + +[customizations] +[customizations.kernel] +name = "kernel" +append = "rootwait console=tty0 console=ttyS0,115200n8 new=argument" + +[customizations.timezone] +timezone = "Etc/UTC" + +[customizations.locale] +languages = [ "C.UTF-8" ]`; +}; + +export const exportedLocaleBP = (blueprintName: string): string => { + return `name = "${blueprintName}" + +[customizations] +[customizations.timezone] +timezone = "Etc/UTC" + +[customizations.locale] +languages = [ "C.UTF-8", "fy_DE.UTF-8", "aa_DJ.UTF-8", "aa_ER.UTF-8" ] +keyboard = "ANSI-dvorak"`; +}; + +export const exportedSystemdBP = (blueprintName: string): string => { + return `name = "${blueprintName}" + +[customizations] +[customizations.services] +enabled = [ "enabled-service" ] +masked = [ "masked-service" ] +disabled = [ "disabled-service" ] + +[customizations.timezone] +timezone = "Etc/UTC" + +[customizations.locale] +languages = [ "C.UTF-8" ]`; +}; + +export const exportedTimezoneBP = (blueprintName: string): string => { + return `name = "${blueprintName}" + +[customizations] +[customizations.timezone] +timezone = "Europe/Oslo" +ntpservers = [ "0.nl.pool.ntp.org", "0.de.pool.ntp.org" ] + +[customizations.locale] +languages = [ "C.UTF-8" ]`; +}; + +export const exportedUsersBP = (blueprintName: string): string => { + return `name = "${blueprintName}" + +[customizations] +[[customizations.user]] +name = "admin1" +key = "" +groups = [ "wheel" ] +password = "" + +[[customizations.user]] +name = "sshuser" +key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCduw9WD1Tw1pat5x+FzMoZGd3QYDcxAPEvgy5shnSzCYsUsO/OTnG2OrN5UXlQ/6fM1Ass5b54ttbsjORxz90ckaKf7W1qufyiuRbDreEYRVabzFDZKeAI5C0pMPya7Fui4vlChsXAH3XuuiJqwtXFjVQbkyI/F9jkVEJZfqo9AAFWF8L33xLXEq/7WfgB9n8NBEL8QX7R8m/ATpKWyOXkWM/welXgGSeRN+dMllwHcX1VnRim0MMXo9JIp39Nl/x9+2fYO8agYyE73zoJj2oueEhBpO9Vam1EziNuEKseIbVzz0VrfZyMeSN5o1+LWYPbCVETE3jUAbioUDxA/faB test@example.com" +groups = [ "developers" ] +password = "" + +[[customizations.user]] +name = "admin2" +key = "" +groups = [ "wheel" ] +password = "" + +[[customizations.user]] +name = "newuser" +key = "" +groups = [] +password = "" + +[customizations.timezone] +timezone = "Etc/UTC" + +[customizations.locale] +languages = [ "C.UTF-8" ]`; +}; diff --git a/playwright/helpers/wizardHelpers.ts b/playwright/helpers/wizardHelpers.ts index d1ef97c00c..00be13b07a 100644 --- a/playwright/helpers/wizardHelpers.ts +++ b/playwright/helpers/wizardHelpers.ts @@ -1,4 +1,6 @@ import * as fs from 'fs'; +import * as fsPromises from 'fs/promises'; +import * as os from 'os'; import * as path from 'path'; import { expect, FrameLocator, type Page, test } from '@playwright/test'; @@ -110,36 +112,58 @@ export const deleteBlueprint = async (page: Page, blueprintName: string) => { /** * Export the blueprint - * This function executes only on the hosted service - * @param page - the page object + * @param page - the page or frame object */ -export const exportBlueprint = async (page: Page, blueprintName: string) => { - if (isHosted()) { - await page.getByRole('button', { name: 'Menu toggle' }).click(); - const downloadPromise = page.waitForEvent('download'); - await page - .getByRole('menuitem', { name: 'Download blueprint (.json)' }) - .click(); - const download = await downloadPromise; - await download.saveAs('../../downloads/' + blueprintName + '.json'); - } +export const exportBlueprint = async (page: Page | FrameLocator): string => { + const frame = await ibFrame(page); + const downloadPromise = page.waitForEvent('download'); + await frame.getByRole('button', { name: 'blueprint menu toggle' }).click(); + await frame + .getByRole('menuitem', { + name: isHosted() + ? 'Download blueprint (.json)' + : 'Download blueprint (.toml)', + }) + .click(); + const download = await downloadPromise; + + const tempDir = await fsPromises.mkdtemp( + path.join(os.tmpdir(), 'export-bp-'), + ); + const filepath = path.join(tempDir, download.suggestedFilename()); + await download.saveAs(filepath); + return filepath; +}; + +/** + * Verify the contents of a blueprint + * @param file - the path to exported blueprint + * @param expectedContent - the expected content of the exported blueprint file + */ +export const verifyExportedBlueprint = ( + filepath: string, + expectedContent: string, +) => { + const content = fs.readFileSync(filepath); + expect(content.toString()).toEqual(expectedContent); }; /** * Import the blueprint * This function executes only on the hosted service * @param page - the page object + * @param blueprintPath - path to a blueprint */ export const importBlueprint = async ( page: Page | FrameLocator, - blueprintName: string, + blueprintPath: string, ) => { if (isHosted()) { await page.getByRole('button', { name: 'Import' }).click(); const dragBoxSelector = page.getByRole('presentation').first(); await dragBoxSelector .locator('input[type=file]') - .setInputFiles('../../downloads/' + blueprintName + '.json'); + .setInputFiles(blueprintPath); await expect( page.getByRole('textbox', { name: 'File upload' }), ).not.toBeEmpty(); @@ -147,16 +171,11 @@ export const importBlueprint = async ( } }; -export const saveBlueprintFileWithContents = async ( - blueprintName: string, - content: string, -) => { - const fixturesDir = path.join(__dirname, '../../../..', 'downloads'); - const tmpFilePath = path.join(fixturesDir, blueprintName + '.json'); - - if (!fs.existsSync(fixturesDir)) { - fs.mkdirSync(fixturesDir, { recursive: true }); - } - - fs.writeFileSync(tmpFilePath, content); +export const saveBlueprintFileWithContents = async (content: string) => { + const tempDir = await fsPromises.mkdtemp( + path.join(os.tmpdir(), 'export-bp-'), + ); + const filepath = path.join(tempDir, 'blueprint.json'); + fs.writeFileSync(filepath, content); + return filepath; }; diff --git a/src/Components/Blueprints/BlueprintActionsMenu.tsx b/src/Components/Blueprints/BlueprintActionsMenu.tsx index f6e4debb1d..19a5a53a06 100644 --- a/src/Components/Blueprints/BlueprintActionsMenu.tsx +++ b/src/Components/Blueprints/BlueprintActionsMenu.tsx @@ -8,8 +8,15 @@ import { } from '@patternfly/react-core'; import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle/MenuToggle'; import { EllipsisVIcon } from '@patternfly/react-icons'; +import TOML from 'smol-toml'; +// The hosted UI exports JSON, while the Cockpit plugin exports TOML. +// Because the blueprint formats differ, using the 'backendApi' +// abstraction would be misleading. Import and handle each environment +// separately. import { selectSelectedBlueprintId } from '../../store/BlueprintSlice'; +import { useLazyExportBlueprintCockpitQuery } from '../../store/cockpit/cockpitApi'; +import type { Blueprint as CockpitExportResponse } from '../../store/cockpit/composerCloudApi'; import { useAppSelector } from '../../store/hooks'; import { BlueprintExportResponse, @@ -30,6 +37,7 @@ export const BlueprintActionsMenu: React.FunctionComponent< }; const [trigger] = useLazyExportBlueprintQuery(); + const [cockpitTrigger] = useLazyExportBlueprintCockpitQuery(); const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId); if (selectedBlueprintId === undefined) { return null; @@ -41,6 +49,15 @@ export const BlueprintActionsMenu: React.FunctionComponent< handleExportBlueprint(response.name, response); }); }; + + const handleCockpitClick = () => { + cockpitTrigger({ id: selectedBlueprintId }) + .unwrap() + .then((response: CockpitExportResponse) => { + handleExportBlueprint(response.name, response); + }); + }; + return ( - - Download blueprint (.json) + + Download blueprint ({process.env.IS_ON_PREMISE ? '.toml' : '.json'}) setShowDeleteModal(true)}> Delete blueprint @@ -75,15 +94,28 @@ export const BlueprintActionsMenu: React.FunctionComponent< async function handleExportBlueprint( blueprintName: string, - blueprint: BlueprintExportResponse, + blueprint: BlueprintExportResponse | CockpitExportResponse, ) { - const jsonData = JSON.stringify(blueprint, null, 2); - const blob = new Blob([jsonData], { type: 'application/json' }); + const data = process.env.IS_ON_PREMISE + ? TOML.stringify(blueprint) + : JSON.stringify(blueprint, null, 2); + const mime = process.env.IS_ON_PREMISE + ? 'application/octet-stream' + : 'application/json'; + const blob = new Blob([data], { type: mime }); const url = URL.createObjectURL(blob); - const link = document.createElement('a'); + // In cockpit we're running in an iframe, the current content-security policy + // (set in cockpit/public/manifest.json) only allows resources from the same origin as the + // document (which is unique to the iframe). So create the element in the parent document. + const link = process.env.IS_ON_PREMISE + ? window.parent.document.createElement('a') + : document.createElement('a'); link.href = url; - link.download = blueprintName.replace(/\s/g, '_').toLowerCase() + '.json'; + link.download = + blueprintName.replace(/\s/g, '_').toLowerCase() + process.env.IS_ON_PREMISE + ? '.toml' + : '.json'; link.click(); URL.revokeObjectURL(url); } diff --git a/src/store/cockpit/cockpitApi.ts b/src/store/cockpit/cockpitApi.ts index f84ffdc4a8..a4ad44cb05 100644 --- a/src/store/cockpit/cockpitApi.ts +++ b/src/store/cockpit/cockpitApi.ts @@ -11,7 +11,10 @@ import { fsinfo } from 'cockpit/fsinfo'; import TOML from 'smol-toml'; import { v4 as uuidv4 } from 'uuid'; -import { Customizations } from './composerCloudApi'; +import type { + Blueprint as CloudApiBlueprint, + Customizations, +} from './composerCloudApi'; // We have to work around RTK query here, since it doesn't like splitting // out the same api into two separate apis. So, instead, we can just // inherit/import the `contentSourcesApi` and build on top of that. @@ -45,6 +48,7 @@ import { DeleteBlueprintApiArg, DeleteBlueprintApiResponse, DistributionProfileItem, + ExportBlueprintApiArg, GetArchitecturesApiArg, GetArchitecturesApiResponse, GetBlueprintApiArg, @@ -407,6 +411,23 @@ export const cockpitApi = contentSourcesApi.injectEndpoints({ } }, }), + exportBlueprintCockpit: builder.query< + CloudApiBlueprint, + ExportBlueprintApiArg + >({ + queryFn: async ({ id }) => { + const blueprintsDir = await getBlueprintsPath(); + const file = cockpit.file(path.join(blueprintsDir, id, `${id}.json`)); + const contents = await file.read(); + const blueprint = JSON.parse( + contents, + ) as CockpitCreateBlueprintRequest; + const onPrem = mapHostedToOnPrem(blueprint as CreateBlueprintRequest); + return { + data: onPrem, + }; + }, + }), getOscapProfiles: builder.query< GetOscapProfilesApiResponse, GetOscapProfilesApiArg @@ -762,6 +783,8 @@ export const { useCreateBlueprintMutation, useUpdateBlueprintMutation, useDeleteBlueprintMutation, + useExportBlueprintCockpitQuery, + useLazyExportBlueprintCockpitQuery, useGetOscapProfilesQuery, useGetOscapCustomizationsQuery, useLazyGetOscapCustomizationsQuery,