Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/e2e_schedule_and_push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
matrix:
user: ["USER_1", "USER_2", "USER_3", "USER_4"]
steps:
- name: install command line utilities
run: sudo apt-get install -y expect
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ x-e2e-runners:
condition: service_healthy
env_file: ./packages/manager/.env
volumes: *default-volumes
entrypoint: ['yarn', 'cy:ci']
entrypoint: ['yarn', 'cy:e2e']

services:
# Serves a local instance of Cloud Manager for Cypress to use for its tests.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"storybook": "yarn workspace linode-manager storybook",
"cy:run": "yarn workspace linode-manager cy:run",
"cy:e2e": "yarn workspace linode-manager cy:e2e",
"cy:ci": "yarn cypress install && yarn cy:e2e",
"cy:ci": "yarn cy:e2e",
"cy:debug": "yarn workspace linode-manager cy:debug",
"cy:rec-snap": "yarn workspace linode-manager cy:rec-snap",
"changeset": "node scripts/changelog/changeset.mjs",
Expand Down Expand Up @@ -70,4 +70,4 @@
"node": "18.14.1"
},
"dependencies": {}
}
}
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-9902-tests-1701186255667.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tests
---

Improve stability for Longview, Rebuild, and Rescue tests ([#9902](https://github.com/linode/manager/pull/9902))
26 changes: 22 additions & 4 deletions packages/manager/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,36 @@ RUN apt-get update \

CMD yarn start:manager:ci

# e2e-build
#
# Builds an image containing Cypress and miscellaneous system utilities required
# by the tests.
FROM cypress/included:13.5.0 as e2e-build
RUN apt-get update \
&& apt-get install -y expect openssh-client \
&& rm -rf /var/cache/apt/* \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean

# e2e-install
#
# Installs Cypress and sets up cache directories.
FROM e2e-build as e2e-install
USER node
WORKDIR /home/node/app
VOLUME /home/node/app
ENV CYPRESS_CACHE_FOLDER=/home/node/.cache/Cypress
RUN mkdir -p /home/node/.cache/Cypress && cypress install

# `e2e`
#
# Runs Cloud Manager Cypress tests.
FROM cypress/included:13.5.0 as e2e
FROM e2e-install as e2e
WORKDIR /home/node/app
VOLUME /home/node/app
USER node
RUN mkdir -p /home/node/.cache/Cypress
ENV CI=1
ENV NO_COLOR=1
ENV HOME=/home/node/
ENV CYPRESS_CACHE_FOLDER=/home/node/.cache/Cypress
ENTRYPOINT yarn cy:ci

# TODO Add `e2e-m1`.
6 changes: 5 additions & 1 deletion packages/manager/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { enableJunitReport } from './cypress/support/plugins/junit-report';
*/
export default defineConfig({
trashAssetsBeforeRuns: false,
projectId: '5rhsif',

// Browser configuration.
chromeWebSecurity: false,
Expand All @@ -35,6 +34,11 @@ export default defineConfig({
defaultCommandTimeout: 80000,
pageLoadTimeout: 60000,

// Recording and test troubleshooting.
projectId: '5rhsif',
screenshotOnRunFailure: true,
video: true,

// Only retry test when running via CI.
retries: process.env['CI'] ? 2 : 0,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const submitRebuild = () => {
ui.button
.findByTitle('Rebuild Linode')
.scrollIntoView()
.should('have.attr', 'data-qa-form-data-loading', 'false')
.should('be.visible')
.should('be.enabled')
.click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
interceptGetLinodeDetails,
interceptRebootLinodeIntoRescueMode,
mockGetLinodeDetails,
mockGetLinodeDisks,
mockGetLinodeVolumes,
mockRebootLinodeIntoRescueModeError,
} from 'support/intercepts/linodes';
import { ui } from 'support/ui';
Expand All @@ -19,6 +21,7 @@ const rebootInRescueMode = () => {
.findByTitle('Reboot into Rescue Mode')
.should('be.visible')
.should('be.enabled')
.should('have.attr', 'data-qa-form-data-loading', 'false')
.click();
};

Expand Down Expand Up @@ -92,6 +95,9 @@ describe('Rescue Linodes', () => {
});

mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode');
mockGetLinodeDisks(mockLinode.id, []).as('getLinodeDisks');
mockGetLinodeVolumes(mockLinode.id, []).as('getLinodeVolumes');

mockRebootLinodeIntoRescueModeError(mockLinode.id, 'Linode busy.').as(
'rescueLinode'
);
Expand Down
219 changes: 168 additions & 51 deletions packages/manager/cypress/e2e/core/longview/longview.spec.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,179 @@
import { LongviewClient } from '@linode/api-v4';
import { randomLabel, randomString } from 'support/util/random';
import { createLinode } from 'support/api/linodes';
import { createClient } from 'support/api/longview';
import { containsVisible, fbtVisible, getVisible } from 'support/helpers';
import { waitForAppLoad } from 'support/ui/common';
import { cleanUp } from 'support/util/cleanup';
import type { Linode, LongviewClient } from '@linode/api-v4';
import { createLongviewClient } from '@linode/api-v4';
import { authenticate } from 'support/api/authentication';
import {
longviewInstallTimeout,
longviewStatusTimeout,
} from 'support/constants/longview';
import {
interceptFetchLongviewStatus,
interceptGetLongviewClients,
} from 'support/intercepts/longview';
import { cleanUp } from 'support/util/cleanup';
import { createAndBootLinode } from 'support/util/linodes';
import { randomLabel, randomString } from 'support/util/random';

// Timeout if Linode creation and boot takes longer than 1 and a half minutes.
const linodeCreateTimeout = 90000;

/**
* Returns the command used to install Longview which is shown in Cloud's UI.
*
* @param installCode - Longview client install code.
*
* @returns Install command string.
*/
const getInstallCommand = (installCode: string): string => {
return `curl -s https://lv.linode.com/${installCode} | sudo bash`;
};

/**
* Installs Longview on a Linode.
*
* @param linodeIp - IP of Linode on which to install Longview.
* @param linodePass - Root password of Linode on which to install Longview.
* @param installCommand - Longview installation command.
*
* @returns Cypress chainable.
*/
const installLongview = (
linodeIp: string,
linodePass: string,
installCommand: string
) => {
return cy.exec('./cypress/support/scripts/longview/install-longview.sh', {
failOnNonZeroExit: true,
timeout: longviewInstallTimeout,
env: {
LINODEIP: linodeIp,
LINODEPASSWORD: linodePass,
CURLCOMMAND: installCommand,
},
});
};

/**
* Waits for Cloud Manager to fetch Longview data and receive updates.
*
* Cloud Manager makes repeated requests to the `/fetch` endpoint, and this
* function waits until one of these requests receives a response for the
* desired Longview client indicating that its data has been updated.
*
* @param alias - Alias assigned to the initial HTTP intercept.
* @param apiKey - API key for Longview client.
*/
const waitForLongviewData = (
alias: string,
apiKey: string,
attempt: number = 0
) => {
const maxAttempts = 50;
// Escape route in case expected response is never received.
if (attempt > maxAttempts) {
throw new Error(
`Timed out waiting for Longview client update after ${maxAttempts} attempts`
);
}
cy.wait(`@${alias}`, { timeout: longviewStatusTimeout }).then(
(interceptedRequest) => {
const responseBody = interceptedRequest.response?.body?.[0];
const apiKeyMatches = (interceptedRequest?.request?.body ?? '').includes(
apiKey
);
const containsUpdate =
responseBody?.ACTION === 'lastUpdated' &&
responseBody?.DATA?.updated !== 0;

if (!(apiKeyMatches && containsUpdate)) {
interceptFetchLongviewStatus().as(alias);
waitForLongviewData(alias, apiKey, attempt + 1);
}
}
);
};

/* eslint-disable sonarjs/no-duplicate-string */
authenticate();
describe('longview', () => {
before(() => {
cleanUp(['linodes', 'longview-clients']);
});

it('tests longview', () => {
const linodePassword = randomString(32);
const clientLabel = randomLabel();
cy.visitWithLogin('/dashboard');
createLinode({ root_pass: linodePassword }).then((linode) => {
createClient(undefined, clientLabel).then((client: LongviewClient) => {
const linodeIp = linode['ipv4'][0];
const clientLabel = client.label;
cy.visit('/longview');
containsVisible(clientLabel);
cy.contains('Waiting for data...').first().should('be.visible');
cy.get('code')
.first()
.then(($code) => {
const curlCommand = $code.text();
cy.exec('./cypress/support/longview.sh', {
failOnNonZeroExit: false,
timeout: 480000,
env: {
LINODEIP: `${linodeIp}`,
LINODEPASSWORD: `${linodePassword}`,
CURLCOMMAND: `${curlCommand}`,
},
}).then((out) => {
console.log(out.stdout);
console.log(out.stderr);
});
waitForAppLoad('/longview', false);
getVisible(`[data-testid="${client.id}"]`).within(() => {
if (
cy
.contains('Waiting for data...', {
timeout: 300000,
})
.should('not.exist')
) {
fbtVisible(clientLabel);
getVisible(`[href="/longview/clients/${client.id}"]`);
containsVisible('Swap');
}
});
});
});
/*
* - Tests Longview installation end-to-end using real API data.
* - Creates a Linode, connects to it via SSH, and installs Longview using the given cURL command.
* - Confirms that Cloud Manager UI updates to reflect Longview installation and data.
*/
it('can install Longview client on a Linode', () => {
const linodePassword = randomString(32, {
symbols: false,
lowercase: true,
uppercase: true,
numbers: true,
spaces: false,
});

const createLinodeAndClient = async () => {
return Promise.all([
createAndBootLinode({
root_pass: linodePassword,
type: 'g6-standard-1',
}),
createLongviewClient(randomLabel()),
]);
};

// Create Linode and Longview Client before loading Longview landing page.
cy.defer(createLinodeAndClient(), {
label: 'Creating Linode and Longview Client...',
timeout: linodeCreateTimeout,
}).then(([linode, client]: [Linode, LongviewClient]) => {
const linodeIp = linode.ipv4[0];
const installCommand = getInstallCommand(client.install_code);

interceptGetLongviewClients().as('getLongviewClients');
interceptFetchLongviewStatus().as('fetchLongviewStatus');
cy.visitWithLogin('/longview');
cy.wait('@getLongviewClients');

// Find the table row for the new Longview client, assert expected information
// is displayed inside of it.
cy.get(`[data-qa-longview-client="${client.id}"]`)
.should('be.visible')
.within(() => {
cy.findByText(client.label).should('be.visible');
cy.findByText(client.api_key).should('be.visible');
cy.contains(installCommand).should('be.visible');
cy.findByText('Waiting for data...');
});

// Install Longview on Linode by SSHing into machine and executing cURL command.
installLongview(linodeIp, linodePassword, installCommand).then(
(output) => {
// TODO Output this to a log file.
console.log(output.stdout);
console.log(output.stderr);
}
);

// Wait for Longview to begin serving data and confirm that Cloud Manager
// UI updates accordingly.
waitForLongviewData('fetchLongviewStatus', client.api_key);

// Sometimes Cloud Manager UI does not updated automatically upon receiving
// Longivew status data. Performing a page reload mitigates this issue.
// TODO Remove call to `cy.reload()`.
cy.reload();
cy.get(`[data-qa-longview-client="${client.id}"]`)
.should('be.visible')
.within(() => {
cy.findByText('Waiting for data...').should('not.exist');
cy.findByText('CPU').should('be.visible');
cy.findByText('RAM').should('be.visible');
cy.findByText('Swap').should('be.visible');
cy.findByText('Load').should('be.visible');
cy.findByText('Network').should('be.visible');
cy.findByText('Storage').should('be.visible');
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,11 @@ describe('Create stackscripts', () => {
interceptGetStackScripts().as('getStackScripts');
interceptCreateLinode().as('createLinode');

cy.visitWithLogin('/stackscripts/create');
cy.defer(createLinodeAndImage(), {
label: 'creating Linode and Image',
timeout: 360000,
}).then((privateImage) => {
cy.visitWithLogin('/stackscripts/create');
cy.fixture(stackscriptBasicPath).then((stackscriptBasic) => {
fillOutStackscriptForm(
stackscriptLabel,
Expand Down
Loading