Skip to content
Open
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
3 changes: 3 additions & 0 deletions integration-tests/cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ module.exports = defineConfig({
specPattern: 'tests/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'support/index.ts',
},
env: {
openshift: true,
},
fixturesFolder: 'fixtures',
reporter: '../../node_modules/cypress-multi-reporters',
reporterOptions: {
Expand Down
183 changes: 183 additions & 0 deletions integration-tests/fixtures/endpoint-health.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Deterministic health fixtures for Services / Routes list E2E.
#
# Healthy: real Deployment + Service (selector-based EndpointSlices created by the control plane)
# Degraded / Down: selector-less Services + handcrafted EndpointSlices (scale-down is flaky for Degraded)
# ExternalName: no endpoints → Unknown
# Route: points at the Healthy service for Backend health assertions
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: eph-healthy-ingress
labels:
app: eph-healthy
spec:
podSelector:
matchLabels:
app: eph-healthy
policyTypes:
- Ingress
ingress:
- ports:
- protocol: TCP
port: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: eph-healthy
labels:
app: eph-healthy
spec:
replicas: 1
selector:
matchLabels:
app: eph-healthy
template:
metadata:
labels:
app: eph-healthy
spec:
automountServiceAccountToken: false
containers:
- name: hello
image: quay.io/openshifttest/hello-openshift:1.2.0
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 1
periodSeconds: 5
resources:
limits:
cpu: 50m
memory: 64Mi
requests:
cpu: 10m
memory: 32Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
---
Comment thread
lkladnit marked this conversation as resolved.
apiVersion: v1
kind: Service
metadata:
name: eph-healthy
labels:
app: eph-healthy
spec:
selector:
app: eph-healthy
ports:
- name: http
port: 80
targetPort: 8080
---
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: eph-healthy
labels:
app: eph-healthy
spec:
to:
kind: Service
name: eph-healthy
port:
targetPort: http
---
apiVersion: v1
kind: Service
metadata:
name: eph-degraded
labels:
app: eph-degraded
spec:
ports:
- name: http
port: 80
targetPort: 8080
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: eph-degraded-slice
labels:
kubernetes.io/service-name: eph-degraded
app: eph-degraded
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 8080
endpoints:
- addresses:
- 192.0.2.1
conditions:
ready: true
- addresses:
- 192.0.2.2
conditions:
ready: false
---
apiVersion: v1
kind: Service
metadata:
name: eph-down
labels:
app: eph-down
spec:
ports:
- name: http
port: 80
targetPort: 8080
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: eph-down-slice
labels:
kubernetes.io/service-name: eph-down
app: eph-down
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 8080
endpoints:
- addresses:
- 192.0.2.10
conditions:
ready: false
- addresses:
- 192.0.2.11
conditions:
ready: false
---
apiVersion: v1
kind: Service
metadata:
name: eph-external
labels:
app: eph-external
spec:
type: ExternalName
externalName: example.com
114 changes: 114 additions & 0 deletions integration-tests/support/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import Loggable = Cypress.Loggable;
import Shadow = Cypress.Shadow;
import Timeoutable = Cypress.Timeoutable;
import Withinable = Cypress.Withinable;

export const MINUTE = 60 * 1000;

export const itemFilter = '[data-test-id="item-filter"]';
export const resourceRow = '[data-test-rows="resource-row"]';

type EndpointHealthStatus = 'Degraded' | 'Down' | 'Healthy' | 'Unknown';

const assertOcSuccess = (
result: {
code: number;
stderr: string;
stdout: string;
},
message: string,
): void => {
// On some macOS/Electron setups Cypress omits `code` even for successful commands.
const code = result.code ?? 0;
expect(code, message).to.eq(0);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const healthAriaLabel = (status: EndpointHealthStatus, ready?: number, total?: number): string => {
if (status === 'Unknown') {
return 'Unknown: endpoint readiness not available';
}
return `${status}: ${ready} of ${total} endpoints ready`;
};

declare global {
namespace Cypress {
interface Chainable {
applyFixture(fixturePath: string, namespace: string): Chainable;
assertEndpointHealth(
name: string,
status: EndpointHealthStatus,
ready?: number,
total?: number,
): Chainable;
byTestID(
selector: string,
options?: Partial<Loggable & Shadow & Timeoutable & Withinable>,
): Chainable;
deleteNamespace(namespace: string): Chainable;
ensureNamespace(namespace: string): Chainable;
filterByName(name: string): Chainable;
getResourceRow(name: string): Chainable;
}
}
}

Cypress.Commands.add(
'byTestID',
(selector: string, options?: Partial<Loggable & Shadow & Timeoutable & Withinable>) =>
cy.get(`[data-test="${selector}"]`, options),
);

Cypress.Commands.add('ensureNamespace', (namespace: string) => {
cy.exec(`oc create namespace ${namespace}`, { failOnNonZeroExit: false }).then((result) => {
const code = result.code ?? 0;
const alreadyExists = /AlreadyExists/i.test(`${result.stderr || ''}${result.stdout || ''}`);
if (code !== 0 && !alreadyExists) {
expect(code, 'failed to create namespace').to.eq(0);
}
});
cy.exec(`oc project ${namespace}`, { failOnNonZeroExit: false }).then((result) => {
assertOcSuccess(result, 'failed to select namespace');
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Cypress.Commands.add('deleteNamespace', (namespace: string) => {
cy.exec(`oc delete namespace ${namespace} --ignore-not-found=true --wait=false`, {
failOnNonZeroExit: false,
timeout: 2 * MINUTE,
});
});

Cypress.Commands.add('applyFixture', (fixturePath: string, namespace: string) => {
cy.exec(`oc apply -n ${namespace} -f "${fixturePath}"`, {
failOnNonZeroExit: false,
timeout: 2 * MINUTE,
}).then((result) => {
assertOcSuccess(result, 'oc apply failed');
});
});

Cypress.Commands.add('filterByName', (name: string) => {
cy.get(itemFilter, { timeout: MINUTE }).should('be.visible').clear();
cy.get(itemFilter, { timeout: MINUTE }).type(name);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Cypress.Commands.add('getResourceRow', (name: string) =>
cy.contains(resourceRow, name, { timeout: MINUTE }).should('exist'),
);

Cypress.Commands.add(
'assertEndpointHealth',
(name: string, status: EndpointHealthStatus, ready?: number, total?: number) => {
const ariaLabel = healthAriaLabel(status, ready, total);

cy.getResourceRow(name).within(() => {
cy.get(`[aria-label="${ariaLabel}"]`, { timeout: MINUTE }).should('exist');

if (status === 'Unknown') {
cy.contains('Unknown').should('exist');
} else if (ready !== undefined && total !== undefined) {
cy.contains(`${ready}/${total}`).should('exist');
}
});
},
);
2 changes: 1 addition & 1 deletion integration-tests/support/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Import commands.js using ES2015 syntax:
import './commands';
import './login';

export const checkErrors = () =>
Expand Down
53 changes: 39 additions & 14 deletions integration-tests/support/login.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,63 @@
declare global {
namespace Cypress {
interface Chainable {
login(username?: string, password?: string): Chainable<Element>;
logout(): Chainable<Element>;
login(providerName?: string, username?: string, password?: string): Chainable<Element>;
logout(): void;
}
}
}

const KUBEADMIN_USERNAME = 'kubeadmin';
const loginUsername = Cypress.env('BRIDGE_KUBEADMIN_PASSWORD') ? 'user-dropdown' : 'username';
const KUBEADMIN_IDP = 'kube:admin';
const TOUR_DISMISS = '[data-test="tour-step-footer-secondary"]';
const MINUTE = 60 * 1000;

// This will add 'cy.login(...)'
// ex: cy.login('my-user', 'my-password')
Cypress.Commands.add('login', (username: string, password: string) => {
// Check if auth is disabled (for a local development environment).
cy.visit('/'); // visits baseUrl which is set in plugins/index.js
Cypress.Commands.add('login', (provider?: string, username?: string, password?: string) => {
const usr = username || KUBEADMIN_USERNAME;
const pwd = password || Cypress.env('BRIDGE_KUBEADMIN_PASSWORD');
const idp = provider || KUBEADMIN_IDP;

cy.visit('/');
cy.window().then((win) => {
if (win.SERVER_FLAGS?.authDisabled) {
return;
}

// Make sure we clear the cookie in case a previous test failed to logout.
cy.clearCookie('openshift-session-token');

cy.get('#inputUsername').type(username || KUBEADMIN_USERNAME);
cy.get('#inputPassword').type(password || Cypress.env('BRIDGE_KUBEADMIN_PASSWORD'));
cy.get('button[type=submit]').click();
cy.origin(
Cypress.config('baseUrl').replace('console-openshift-console', 'oauth-openshift'),
{ args: { idp, pwd, usr } },
({ idp: originIdp, pwd: originPwd, usr: originUsr }) => {
cy.get('body', { timeout: 180000 }).should('be.visible');
cy.get('body').then(($body) => {
if ($body.find('#inputUsername').length === 0) {
if ($body.text().includes(originIdp)) {
cy.contains('a', originIdp).click();
} else if ($body.text().includes('kubeadmin')) {
cy.contains('a', 'kubeadmin').click();
} else {
cy.get('a').first().click();
}
}
});
cy.get('#inputUsername', { timeout: 180000 }).should('be.visible');
cy.get('#inputUsername').type(originUsr);
cy.get('#inputPassword').type(originPwd, { log: false });
cy.get('button[type=submit]').click();
},
);

cy.get(`[data-test="${loginUsername}"]`).should('be.visible');
cy.url({ timeout: 2 * MINUTE }).should('include', 'console-openshift-console');
cy.get('body').then(($body) => {
if ($body.find(TOUR_DISMISS).length) {
cy.get(TOUR_DISMISS).click();
}
});
});
});

Cypress.Commands.add('logout', () => {
// Check if auth is disabled (for a local development environment).
cy.window().then((win) => {
if (win.SERVER_FLAGS?.authDisabled) {
return;
Expand Down
Loading