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
7 changes: 7 additions & 0 deletions portals/developer-portal/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
# node_modules is installed fresh inside the container by npm ci
node_modules

# ── Local dev SQLite databases — must never be baked into the image. A stale
# copy here is root-owned (COPY runs before the chown to appuser) and silently
# shadows the real runtime DB at the configured database.file path, causing
# every write (sessions, seeding, queues) to fail as "readonly database".
*.db
data/

# ── Config / secrets (mounted at runtime via volume) ──────────────
config.toml
configs/
Expand Down
1 change: 1 addition & 0 deletions portals/developer-portal/it/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
.certs/
reports/videos/
reports/screenshots/
reports/*.json
Expand Down
32 changes: 27 additions & 5 deletions portals/developer-portal/it/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,39 @@
# under the License.
# --------------------------------------------------------------------

.PHONY: all test test-postgres test-rest-api test-rest-api-postgres open clean deps ensure-test-tag
.PHONY: all test test-postgres test-rest-api test-rest-api-postgres open clean deps ensure-test-tag ensure-certs

VERSION ?= $(shell cat ../VERSION 2>/dev/null | tr -d '[:space:]' || echo "0.0.1-SNAPSHOT")
DOCKER_REGISTRY ?= ghcr.io/wso2/api-platform
IMAGE_NAME := developer-portal
CERT_DIR := .certs

# Auto-detect host CPU so Cypress runs natively (no QEMU).
# arm64 = Apple Silicon → pull linux/arm64 Cypress image.
# Everything else (CI, Intel) defaults to linux/amd64.
CYPRESS_PLATFORM ?= $(shell [ "$$(uname -m)" = "arm64" ] && echo "linux/arm64" || echo "linux/amd64")

# One-time, idempotent TLS cert generation for platform-api's HTTPS listener —
# mirrors ../scripts/setup.sh's openssl step, scoped to this test fixture and run
# automatically as a `make test`/`test-postgres`/`test-rest-api*` prerequisite
# (unlike setup.sh, which a person runs by hand before `docker compose up`) so CI
# and a fresh checkout need no manual pre-step. Bind-mounted read-only into
# platform-api (docker-compose.test*.yaml) instead of the old certgen init
# container + named volume. 644, not 600, so the container's non-root user can
# read a file owned by the host user (same tradeoff as setup.sh's CERT_FILE_MODE).
ensure-certs:
@mkdir -p $(CERT_DIR)
@if [ -f $(CERT_DIR)/cert.pem ] && [ -f $(CERT_DIR)/key.pem ]; then \
echo "Using existing $(CERT_DIR)/{cert,key}.pem"; \
else \
echo "Generating platform-api TLS certificate at $(CERT_DIR) ..."; \
openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -nodes \
-keyout $(CERT_DIR)/key.pem -out $(CERT_DIR)/cert.pem \
-subj "/O=WSO2 API Platform/CN=platform-api" \
-addext "subjectAltName=DNS:localhost,DNS:platform-api,IP:127.0.0.1" 2>/dev/null; \
chmod 644 $(CERT_DIR)/key.pem $(CERT_DIR)/cert.pem; \
fi

# Tag the pre-built developer-portal image as :test so docker-compose.test.yaml can
# reference a stable :test tag regardless of whether the local build is :VERSION.
ensure-test-tag:
Expand All @@ -50,30 +72,30 @@ ensure-test-tag:
# docker inspect`: `ps -q` lists only RUNNING containers, so once the service has
# exited it returns nothing and the code is silently lost (the target then passes
# even when tests fail). Keep the capture in the same shell as `up`.
test: ensure-test-tag
test: ensure-test-tag ensure-certs
@CYPRESS_PLATFORM=$(CYPRESS_PLATFORM) DOCKER_REGISTRY=$(DOCKER_REGISTRY) docker compose -f docker-compose.test.yaml up devportal cypress --abort-on-container-exit --exit-code-from cypress; \
EXIT=$$?; \
docker compose -f docker-compose.test.yaml down -v --remove-orphans; \
exit $$EXIT

# Run Cypress tests headlessly against PostgreSQL.
# Requires the developer-portal image to be built first: make build (from portals/developer-portal/).
test-postgres: ensure-test-tag
test-postgres: ensure-test-tag ensure-certs
@CYPRESS_PLATFORM=$(CYPRESS_PLATFORM) DOCKER_REGISTRY=$(DOCKER_REGISTRY) docker compose -f docker-compose.test.postgres.yaml up postgres devportal cypress --abort-on-container-exit --exit-code-from cypress; \
EXIT=$$?; \
docker compose -f docker-compose.test.postgres.yaml down -v --remove-orphans; \
exit $$EXIT

# Run the REST API integration test suite (Jest + Supertest) against SQLite.
# Requires the developer-portal image to be built first: make build (from portals/developer-portal/).
test-rest-api: ensure-test-tag
test-rest-api: ensure-test-tag ensure-certs
@DOCKER_REGISTRY=$(DOCKER_REGISTRY) docker compose -f docker-compose.test.yaml up devportal rest-api-tests --abort-on-container-exit --exit-code-from rest-api-tests; \
EXIT=$$?; \
docker compose -f docker-compose.test.yaml down -v --remove-orphans; \
exit $$EXIT

# Run the REST API integration test suite (Jest + Supertest) against PostgreSQL.
test-rest-api-postgres: ensure-test-tag
test-rest-api-postgres: ensure-test-tag ensure-certs
@DOCKER_REGISTRY=$(DOCKER_REGISTRY) docker compose -f docker-compose.test.postgres.yaml up postgres devportal rest-api-tests --abort-on-container-exit --exit-code-from rest-api-tests; \
EXIT=$$?; \
docker compose -f docker-compose.test.postgres.yaml down -v --remove-orphans; \
Expand Down
28 changes: 5 additions & 23 deletions portals/developer-portal/it/docker-compose.test.postgres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,36 +41,19 @@ services:
networks:
- it-devportal-network

# One-shot init container: generates the TLS pair the platform-api HTTPS
# listener requires (the server no longer generates a self-signed fallback).
platform-api-certgen:
image: alpine/openssl
entrypoint: ["/bin/sh", "-c"]
command:
- |
if [ ! -f /certs/cert.pem ] || [ ! -f /certs/key.pem ]; then
openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -nodes \
-keyout /certs/key.pem -out /certs/cert.pem \
-subj "/O=WSO2 API Platform/CN=platform-api" \
-addext "subjectAltName=DNS:localhost,DNS:platform-api,IP:127.0.0.1"
fi
chown 10001:10001 /certs/cert.pem /certs/key.pem
volumes:
- platform-api-certs:/certs

platform-api:
image: ghcr.io/wso2/api-platform/platform-api:0.11.0
image: ghcr.io/wso2/api-platform/platform-api:0.12.0
command: ["-config", "/etc/platform-api/config-platform-api.toml"]
environment:
APIP_CP_ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
APIP_CP_AUTH_JWT_SECRET_KEY: "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
volumes:
- ./configs/config-platform-api-it.toml:/etc/platform-api/config-platform-api.toml:ro
- platform-api-data:/app/data
- platform-api-certs:/app/data/certs
depends_on:
platform-api-certgen:
condition: service_completed_successfully
# TLS pair generated once, host-side, by `make ensure-certs` (mirrors
# ../scripts/setup.sh's approach for the production compose) — read-only,
# 644-mode so the container's non-root user can read a host-owned file.
- ./.certs:/app/data/certs:ro
healthcheck:
test: ["CMD", "curl", "-fk", "https://localhost:9243/health"]
interval: 5s
Expand Down Expand Up @@ -168,5 +151,4 @@ networks:
driver: bridge

volumes:
platform-api-certs:
platform-api-data:
28 changes: 5 additions & 23 deletions portals/developer-portal/it/docker-compose.test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,19 @@
# The devportal service uses the pre-built image tagged :test (set by make ensure-test-tag).

services:
# One-shot init container: generates the TLS pair the platform-api HTTPS
# listener requires (the server no longer generates a self-signed fallback).
platform-api-certgen:
image: alpine/openssl
entrypoint: ["/bin/sh", "-c"]
command:
- |
if [ ! -f /certs/cert.pem ] || [ ! -f /certs/key.pem ]; then
openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -nodes \
-keyout /certs/key.pem -out /certs/cert.pem \
-subj "/O=WSO2 API Platform/CN=platform-api" \
-addext "subjectAltName=DNS:localhost,DNS:platform-api,IP:127.0.0.1"
fi
chown 10001:10001 /certs/cert.pem /certs/key.pem
volumes:
- platform-api-certs:/certs

platform-api:
image: ghcr.io/wso2/api-platform/platform-api:0.11.0
image: ghcr.io/wso2/api-platform/platform-api:0.12.0
command: ["-config", "/etc/platform-api/config-platform-api.toml"]
environment:
APIP_CP_ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
APIP_CP_AUTH_JWT_SECRET_KEY: "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
volumes:
- ./configs/config-platform-api-it.toml:/etc/platform-api/config-platform-api.toml:ro
- platform-api-data:/app/data
- platform-api-certs:/app/data/certs
depends_on:
platform-api-certgen:
condition: service_completed_successfully
# TLS pair generated once, host-side, by `make ensure-certs` (mirrors
# ../scripts/setup.sh's approach for the production compose) — read-only,
# 644-mode so the container's non-root user can read a host-owned file.
- ./.certs:/app/data/certs:ro
healthcheck:
test: ["CMD", "curl", "-fk", "https://localhost:9243/health"]
interval: 5s
Expand Down Expand Up @@ -154,6 +137,5 @@ networks:
driver: bridge

volumes:
platform-api-certs:
sqlite-data:
platform-api-data:
15 changes: 14 additions & 1 deletion portals/developer-portal/it/rest-api/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,20 @@ module.exports = {
// Registers a per-suite afterAll that deletes resources tracked via
// support/cleanup.js, so specs don't accumulate objects in the shared org.
setupFilesAfterEnv: ['<rootDir>/support/autoCleanup.js'],
testTimeout: 10000,
testTimeout: 20000,
// The devportal-under-test runs SQLite through a single Sequelize connection
// (pool: { max: 1 } — see src/db/sequelizeConfig.js, correct for a single-writer
// SQLite file) shared by every model AND the session store. Because
// better-sqlite3 is synchronous, that one connection processes every DB op —
// logins' session writes, fixture CRUD from every spec file, the webhook
// dispatcher/delivery-worker polling — strictly one at a time regardless of
// Jest's worker count, so parallel workers add queuing/coordination risk
// without any real throughput gain. Under load, queued ops can exceed the
// test timeout and surface as spurious "Login failed" redirects
// (authController.js's session.regenerate/logIn failure branches), not real
// auth bugs — this was flaky even at maxWorkers: 2 depending on host load.
// Fully serial removes the contention risk entirely.
maxWorkers: 1,
reporters: [
'default',
['jest-junit', { outputDirectory: 'reports', outputName: 'rest-api-results.xml' }],
Expand Down
24 changes: 24 additions & 0 deletions portals/developer-portal/it/test-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
# at /app/configs/config.toml in both. Settings that are identical across both DB
# variants live here; settings that differ (database connection, network hostnames)
# stay as APIP_DP_* env vars in each compose file's environment: block.
#
# There is no automatic APIP_DP_* environment-variable override (see
# src/config/configLoader.js / configs/config.toml) — an env var set in either
# compose file's environment: block only takes effect where a key below
# explicitly references it via a {{ env "..." "default" }} token. Every key
# that either compose file overrides MUST have a matching token here, or the
# override is silently ignored and the built-in default
# (src/config/configDefaults.js) is used instead.

[server]
base_url = '{{ env "APIP_DP_SERVER_BASEURL" "http://devportal:3000" }}'

[tls]
# Use plain HTTP inside the test network so Cypress doesn't need to trust a cert.
Expand All @@ -12,6 +23,19 @@ enabled = false
[logging]
console_only = true

[database]
# type/file cover docker-compose.test.yaml (SQLite); host/port/username/password/name
# cover docker-compose.test.postgres.yaml (PostgreSQL) — each compose file only sets
# the env vars relevant to its own variant, so the other side's tokens just resolve
# to these defaults, unused.
type = '{{ env "APIP_DP_DATABASE_TYPE" "sqlite" }}'
file = '{{ env "APIP_DP_DATABASE_FILE" "./data/devportal-it.db" }}'
host = '{{ env "APIP_DP_DATABASE_HOST" "localhost" }}'
port = '{{ env "APIP_DP_DATABASE_PORT" "5432" }}'
username = '{{ env "APIP_DP_DATABASE_USERNAME" "postgres" }}'
password = '{{ env "APIP_DP_DATABASE_PASSWORD" "" }}'
name = '{{ env "APIP_DP_DATABASE_NAME" "devportal" }}'

[security]
# Required — devportal fails closed at startup without these.
encryption_key = '{{ env "APIP_DP_SECURITY_ENCRYPTIONKEY" }}'
Expand Down
7 changes: 6 additions & 1 deletion portals/developer-portal/it/ui/cypress/support/e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ before(() => {
const apiKey = Cypress.env('API_KEY');
cy.request({
method: 'GET',
url: '/organizations',
// Must include the API base path — the devportal API router (see
// src/routes/api/devportalApiRouter.js) only recognizes requests whose
// first path segment matches the OpenAPI spec's server basePath ('api').
// A bare '/organizations' falls through to the page-rendering route
// tree instead, which misinterprets "organizations" as an org handle.
url: '/api/v0.9/organizations',
headers: apiKey ? { 'x-wso2-api-key': apiKey } : {},
failOnStatusCode: false,
}).then((resp) => {
Expand Down
25 changes: 23 additions & 2 deletions portals/developer-portal/src/dao/viewDao.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
const View = require('../models/view');
const ViewLabels = require('../models/viewLabel');
const Labels = require('../models/label');
const { OrgContent } = require('../models/organization');
const { Sequelize } = require('sequelize');
const constants = require('../utils/constants');
const { CustomError } = require('../utils/errors/customErrors');
Expand Down Expand Up @@ -76,14 +77,34 @@ const update = async (orgId, handle, displayName, updatedBy, t) => {
}
}

const deleteView = async (orgId, handle) => {
const deleteView = async (orgId, handle, t) => {

try {
const view = await View.findOne({
where: {
handle: handle,
org_uuid: orgId
},
transaction: t
});
if (!view) {
return 0;
}
// Neither dependent's view_uuid FK actually cascades at the SQLite level —
// dp_view_label_mappings (models/viewLabel.js) has no onDelete at all, and
// dp_organization_assets (models/organization.js's OrgContent) only sets
// onDelete: 'CASCADE' on the Sequelize association, which doesn't translate
// into the generated SQLite constraint. Destroying a view with either still
// attached fails with a FOREIGN KEY constraint error — detach both first,
// same pattern organizationDao's whole-org delete already uses for OrgContent.
await ViewLabels.destroy({ where: { view_uuid: view.dataValues.uuid }, transaction: t });
await OrgContent.destroy({ where: { view_uuid: view.dataValues.uuid }, transaction: t });
const viewResponse = await View.destroy({
where: {
handle: handle,
org_uuid: orgId
}
},
transaction: t
Comment thread
lasanthaS marked this conversation as resolved.
});
return viewResponse;
} catch (error) {
Expand Down
5 changes: 5 additions & 0 deletions portals/developer-portal/src/services/apiMetadataService.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const subDao = require('../dao/subscriptionDao');
const labelDao = require('../dao/labelDao');
const tagDao = require('../dao/tagDao');
const viewDao = require('../dao/viewDao');
const apiWorkflowDao = require('../dao/apiWorkflowDao');
const subscriptionPlanDao = require('../dao/subscriptionPlanDao');
const apiFileDao = require('../dao/apiFileDao');
const apiKeyDao = require("../dao/apiKeyDao");
Expand Down Expand Up @@ -1704,6 +1705,10 @@ const deleteView = async (req, res) => {
throw new CustomError(400, constants.ERROR_CODE[400], "The default view cannot be deleted");
}
const viewUuid = await viewDao.getId(orgId, name);
const workflows = await apiWorkflowDao.list(orgId, viewUuid);
if (workflows.length > 0) {
throw new CustomError(409, constants.ERROR_MESSAGE.ERR_WORKFLOW_EXIST, "View has API workflows.");
}
const viewDelete = await viewDao.delete(orgId, name);
if (viewDelete === 0) {
throw new Sequelize.EmptyResultError("Resource not found to delete");
Expand Down
1 change: 1 addition & 0 deletions portals/developer-portal/src/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ module.exports = {
WEBHOOK_SUBSCRIBER_NOT_FOUND: "Webhook subscriber not found",
ERR_SUB_EXIST: "ERR_SUB_EXIST",
ERR_KEY_EXIST: "ERR_KEY_EXIST",
ERR_WORKFLOW_EXIST: "ERR_WORKFLOW_EXIST",
UNAUTHORIZED_ORG: "You are not authorized to access this organization",
UNAUTHORIZED_API: "You are not authorized to access this API",
API_NOT_FOUND: "Requested API not found",
Expand Down
Loading