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
14 changes: 13 additions & 1 deletion portals/developer-portal/it/ui/cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// --------------------------------------------------------------------

const { defineConfig } = require('cypress');
const { startMockTokenServer, stopMockTokenServer } = require('./mock-token-server');

module.exports = defineConfig({
e2e: {
Expand All @@ -33,7 +34,7 @@ module.exports = defineConfig({
responseTimeout: 15000,
// Accept self-signed certs from the devportal
chromeWebSecurity: false,
setupNodeEvents(on) {
setupNodeEvents(on, config) {
// Pass required flags to Chrome/Chromium in Docker (no sandbox, no GPU)
on('before:browser:launch', (browser, launchOptions) => {
if (browser.family === 'chromium') {
Expand All @@ -43,6 +44,17 @@ module.exports = defineConfig({
}
return launchOptions;
});

// On-demand mock OAuth2 token endpoint for the application key/token
// round-trip test. Registered as tasks (not started here) so it only
// listens while a test that needs it is running — the test starts it in
// its before() and stops it in after(); it returns { endpoint, secret,
// accessToken } for the seeded key manager to point at.
on('task', {
startMockTokenServer: () => startMockTokenServer(),
stopMockTokenServer: () => stopMockTokenServer(),
});
return config;
},
},
env: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@
// --------------------------------------------------------------------

describe('Developer Portal — Portal Access', () => {
// Prereq: the default org + view are auto-seeded by the devportal on startup
// (seederService.seedDefaultOrg), but the API/MCP listings are empty until
// something is deployed — so we seed one PUBLISHED REST API and one MCP
// server into the default view via the management API, and tear them down
// afterward. The REST API's definition declares an apiKey security scheme so
// its detail page renders the auth-gated "API Keys" action used below.
let apiHandle;
let mcpHandle;

before(() => {
cy.seedApi().then((handle) => { apiHandle = handle; });
cy.seedMcp().then((handle) => { mcpHandle = handle; });
});

after(() => {
cy.deleteApi(apiHandle);
cy.deleteMcp(mcpHandle);
});

beforeEach(() => {
cy.on('uncaught:exception', () => false);
});
Expand All @@ -25,4 +44,74 @@ describe('Developer Portal — Portal Access', () => {
cy.visitPortal();
cy.get('.hero').should('be.visible');
});

// -----------------------------------------------------------------------
// Basic browsing as an anonymous (logged-out) visitor. The APIs / MCP
// listings and the API detail page are public — no login required.
// -----------------------------------------------------------------------
context('basic browsing (logged out)', () => {
it('browses the APIs listing and opens an API detail page', () => {
cy.visitPortal('/apis');
// At least the seeded API is present in the default view.
cy.get('.api-card').should('have.length.at.least', 1);

// Click into an API — the card's [data-href] region drives navigation.
cy.get('.api-card').first().find('.api-card-top').click();

// The API detail page loaded.
cy.url().should('include', '/api/');
cy.get('.aov-hero-name').should('be.visible');
});

it('browses the MCP servers listing', () => {
cy.visitPortal('/mcps');
cy.get('.apilist-results-heading').should('contain', 'MCP Servers');
cy.get('.api-card').should('have.length.at.least', 1);
});

it('opens the API Workflows page', () => {
cy.visitPortal('/api-workflows');
cy.get('body').should('be.visible');
cy.get('body').should('not.contain.text', 'Cannot GET');
cy.get('body').should('not.contain.text', '500');
});
});

// -----------------------------------------------------------------------
// Auth-gated pages redirect an anonymous visitor to the login page and,
// after a successful login, send them back to the page they asked for
// (server-side `returnTo`).
// -----------------------------------------------------------------------
context('auth redirection (logged out)', () => {
it('redirects Applications to login and returns there after signing in', () => {
cy.visitPortal();

// Applications is auth-gated → clicking it bounces to the login page.
cy.get('#sidebar #applications').click();
cy.url().should('include', '/login');
cy.get('#local-login-form').should('be.visible');

// After logging in, land back on the originally requested page.
cy.completeLoginForm();
cy.url().should('include', '/applications');
cy.get('#local-login-form').should('not.exist');
cy.contains('.page-title', 'Applications').should('be.visible');
});

it('redirects an API\'s API Keys page to login and returns there after signing in', () => {
// Open the seeded API's detail page (public).
cy.visitPortal(`/api/${apiHandle}`);
cy.get('.aov-hero-name').should('be.visible');

// The API Keys action is auth-gated → clicking it bounces to login.
cy.get('.aov-hero-actions a[href$="/api-keys"]').click();
cy.url().should('include', '/login');
cy.get('#local-login-form').should('be.visible');

// After logging in, land back on that API's API Keys page.
cy.completeLoginForm();
cy.url().should('include', `/api/${apiHandle}/api-keys`);
cy.get('#local-login-form').should('not.exist');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// --------------------------------------------------------------------
// Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// --------------------------------------------------------------------

// Sidebar behaviour. The sidebar is collapsed by default; the expand/collapse
// button (#collapseBtn) pins it open (class `expanded`) or force-collapses it
// (class `force-collapse`), persisting the choice in localStorage.
//
// NOTE: hover-to-open / stay-while-inside / close-on-leave are driven purely by
// CSS `:hover` (.sidebar:hover:not(.force-collapse)), which cannot be triggered
// by Cypress without the cypress-real-events plugin. This suite covers the
// JS/localStorage-driven behaviour instead: collapsed-by-default, the nav items,
// pin-to-open via the button, persistence across navigation + reload, and
// collapse via the button.

describe('Developer Portal — Sidebar', () => {
beforeEach(() => {
cy.on('uncaught:exception', () => false);
cy.visitPortal(); // portal home
});

it('is collapsed by default and lists the navigation items', () => {
cy.get('#sidebar')
.should('be.visible')
.and('not.have.class', 'expanded');
// Collapsed width (~4.625rem).
cy.get('#sidebar').invoke('outerWidth').should('be.lessThan', 140);
cy.get('#collapseBtn .collapse-text').should('contain', 'Expand');

// Navigation items are present.
cy.get('#sidebar #home').should('exist');
cy.get('#sidebar #apis').should('exist');
cy.get('#sidebar #mcps').should('exist');
cy.get('#sidebar #api-workflows').should('exist');
cy.get('#sidebar #applications').should('exist');
});

it('pins open via the expand button and stays open across navigation and reload', () => {
// Click expand → the sidebar is pinned open (independent of the mouse).
cy.get('#collapseBtn').click();
cy.get('#sidebar').should('have.class', 'expanded');
cy.get('#collapseBtn .collapse-text').should('contain', 'Collapse');
cy.get('#sidebar').invoke('outerWidth').should('be.greaterThan', 180);

// Navigate to APIs — the pinned state persists (localStorage) ...
cy.get('#sidebar #apis').click();
cy.url().should('include', '/apis');
cy.get('#sidebar').should('have.class', 'expanded');

// ... and survives a full reload.
cy.reload();
cy.get('#sidebar').should('have.class', 'expanded');
});

it('collapses via the collapse button', () => {
// Pin open first.
cy.get('#collapseBtn').click();
cy.get('#sidebar').should('have.class', 'expanded');

// Click again → force-collapsed (stays collapsed even on hover).
cy.get('#collapseBtn').click();
cy.get('#sidebar')
.should('not.have.class', 'expanded')
.and('have.class', 'force-collapse');
cy.get('#collapseBtn .collapse-text').should('contain', 'Expand');
cy.get('#sidebar').invoke('outerWidth').should('be.lessThan', 140);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// --------------------------------------------------------------------
// Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// --------------------------------------------------------------------

// The /apis listing page: renders a card per published API (in the default view)
// with a type badge, and supports server-side search (Enter navigates to
// ?query=... and the server filters). The listing is public — no login needed.

describe('API listing', () => {
const REST_API = 'IT Listing REST API';
const GQL_API = 'IT Listing GraphQL API';
const MCP_NAME = 'IT Listing MCP Server';
let restHandle;
let gqlHandle;
let mcpHandle;

before(() => {
// Seed two APIs of different types so the listing shows distinct badges,
// plus an MCP server — which must NOT appear on the /apis listing (it
// belongs on /mcps). loadAPIs filters type !== MCP for the APIs page.
cy.seedApi({ name: REST_API }).then((h) => { restHandle = h; });
cy.seedApi({
name: GQL_API,
type: 'GRAPHQL',
definition: 'type Query { hello: String }\n',
definitionFileName: 'schema.graphql',
definitionContentType: 'application/graphql',
}).then((h) => { gqlHandle = h; });
cy.seedMcp({ name: MCP_NAME }).then((h) => { mcpHandle = h; });
});

after(() => {
cy.deleteApi(restHandle);
cy.deleteApi(gqlHandle);
cy.deleteMcp(mcpHandle);
});

beforeEach(() => {
cy.on('uncaught:exception', () => false);
});

it('lists API cards, including different API types', () => {
cy.visitPortal('/apis');

cy.get('.apilist-results-heading').should('contain', 'APIs');
cy.get('.api-card').should('have.length.at.least', 2);

// Both seeded APIs appear, each with its own type badge.
cy.contains('.api-card', REST_API).within(() => {
cy.get('.dp-badge--rest').should('exist').and('contain', 'REST');
});
cy.contains('.api-card', GQL_API).within(() => {
cy.get('.dp-badge--graphql').should('exist').and('contain', 'GraphQL');
});

// MCP servers belong on /mcps and must not appear in the API listing.
cy.contains('.api-card', MCP_NAME).should('not.exist');
});

it('narrows the listing with a search query', () => {
cy.visitPortal('/apis');

// Search is server-side: Enter navigates to ?query=... and the server filters.
cy.get('#query').type(`${GQL_API}{enter}`);
cy.url().should('include', 'query=');

// Only the matching API remains; the non-matching one is filtered out.
cy.contains('.api-card', GQL_API).should('be.visible');
cy.contains('.api-card', REST_API).should('not.exist');
});

it('does not surface MCP servers via API search', () => {
cy.visitPortal('/apis');

// Searching an MCP server's name from the APIs page returns no card —
// MCP-typed results are filtered out of the /apis listing and search.
cy.get('#query').type(`${MCP_NAME}{enter}`);
cy.url().should('include', 'query=');
cy.contains('.api-card', MCP_NAME).should('not.exist');
});
});
Loading
Loading