From bd23b000ab647bbfed8e6849c08ea3d57dbf4d5f Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Mon, 23 Mar 2026 12:14:04 +0100 Subject: [PATCH 01/25] Migrate github discovery Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/playwright.config.ts | 4 + .../e2e-tests/support/api/github-helper.ts | 80 +++++++++++++++++++ .../support/api/github-structures.ts | 3 + .../support/constants/github/organization.ts | 1 + .../github-discovery/app-config-rhdh.yaml | 38 +++++++++ .../github-discovery/dynamic-plugins.yaml | 3 + .../config/github-discovery/rhdh-secrets.yaml | 13 +++ .../tests/specs/github-discovery.spec.ts | 61 ++++++++++++++ 8 files changed, 203 insertions(+) create mode 100644 workspaces/backstage/e2e-tests/support/api/github-helper.ts create mode 100644 workspaces/backstage/e2e-tests/support/api/github-structures.ts create mode 100644 workspaces/backstage/e2e-tests/support/constants/github/organization.ts create mode 100644 workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml create mode 100644 workspaces/backstage/e2e-tests/tests/config/github-discovery/dynamic-plugins.yaml create mode 100644 workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml create mode 100644 workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts diff --git a/workspaces/backstage/e2e-tests/playwright.config.ts b/workspaces/backstage/e2e-tests/playwright.config.ts index 55397b020..44dcce0af 100644 --- a/workspaces/backstage/e2e-tests/playwright.config.ts +++ b/workspaces/backstage/e2e-tests/playwright.config.ts @@ -9,6 +9,10 @@ dotenv.config({ path: `${import.meta.dirname}/.env` }); */ export default defineConfig({ projects: [ + { + name: "backstage-github-discovery", + testMatch: /tests\/specs\/github-discovery\.spec\.ts/, + }, { name: "backstage-github-events", testMatch: /tests\/specs\/github-events-module\.spec\.ts/, diff --git a/workspaces/backstage/e2e-tests/support/api/github-helper.ts b/workspaces/backstage/e2e-tests/support/api/github-helper.ts new file mode 100644 index 000000000..257339d2e --- /dev/null +++ b/workspaces/backstage/e2e-tests/support/api/github-helper.ts @@ -0,0 +1,80 @@ +import { APIResponse, request } from "@playwright/test"; +import { GetOrganizationResponse } from "./github-structures"; +import { RHDH_GITHUB_TEST_ORGANIZATION } from "../constants/github/organization"; + +// https://docs.github.com/en/rest?apiVersion=2022-11-28 +export class GithubApiHelper { + private readonly apiUrl = "https://api.github.com"; + private readonly apiVersion = "2022-11-28"; + private readonly authHeader = { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${process.env.VAULT_GH_RHDH_QE_USER_TOKEN}`, + "X-GitHub-Api-Version": this.apiVersion, + }; + + public async getOrganization( + org = process.env.GITHUB_TEST_ORGANIZATION ?? RHDH_GITHUB_TEST_ORGANIZATION, + ): Promise { + const req = await this._organization(org).get(); + const data = (await req.json()) as Record; + const reposUrl = data["repos_url"]; + if (typeof reposUrl !== "string" || reposUrl.length === 0) { + throw new Error( + "Invalid GitHub organization response: missing repos_url", + ); + } + return { reposUrl }; + } + + public async getReposFromOrg( + org = process.env.GITHUB_TEST_ORGANIZATION ?? RHDH_GITHUB_TEST_ORGANIZATION, + ) { + const req = await this._organization(org).repos(); + return req.json(); + } + + public async fileExistsOnRepo(repo: string, file: string): Promise { + const req = await this._repo(repo).getContent(file); + const status = req.status(); + if (status == 403) { + throw Error("You don-t have permissions to see this path"); + } + return [200, 302, 304].includes(status); + } + + private _myContext = request.newContext({ + baseURL: this.apiUrl, + extraHTTPHeaders: this.authHeader, + }); + + private _repo(repo: string) { + const url = `/repos/${repo}/`; + return { + getContent: async (path: string) => { + path = url + path; + const context = await this._myContext; + return context.get(path); + }, + }; + } + + private _organization(organization: string) { + const url = "/orgs/"; + + return { + get: async (): Promise => { + const path: string = url + organization; + const context = await this._myContext; + return context.get(path); + }, + + repos: async (): Promise => { + const context = await this._myContext; + const organizationResponse = await new GithubApiHelper() + ._organization(organization) + .get(); + return context.get((await organizationResponse.json()).repos_url); + }, + }; + } +} diff --git a/workspaces/backstage/e2e-tests/support/api/github-structures.ts b/workspaces/backstage/e2e-tests/support/api/github-structures.ts new file mode 100644 index 000000000..3b235a037 --- /dev/null +++ b/workspaces/backstage/e2e-tests/support/api/github-structures.ts @@ -0,0 +1,3 @@ +export type GetOrganizationResponse = { + reposUrl: string; +}; diff --git a/workspaces/backstage/e2e-tests/support/constants/github/organization.ts b/workspaces/backstage/e2e-tests/support/constants/github/organization.ts new file mode 100644 index 000000000..11946beda --- /dev/null +++ b/workspaces/backstage/e2e-tests/support/constants/github/organization.ts @@ -0,0 +1 @@ +export const RHDH_GITHUB_TEST_ORGANIZATION = "janus-qe"; diff --git a/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml b/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml new file mode 100644 index 000000000..d13079157 --- /dev/null +++ b/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml @@ -0,0 +1,38 @@ +app: + title: RHDH Backstage Github Discovery Test Instance + +catalog: + rules: + - allow: + [ + Component, + API, + Resource, + System, + Domain, + Location, + Template, + User, + Group, + ] + providers: + github: + providerId: + organization: $GITHUB_TEST_ORGANIZATION + schedule: + frequency: + hours: 24 + initialDelay: + seconds: 15 + timeout: + minutes: 10 + +integrations: + github: + - host: github.com + apps: + - appId: $VAULT_GITHUB_APP_APP_ID + clientId: $VAULT_GITHUB_APP_CLIENT_ID + clientSecret: $VAULT_GITHUB_APP_CLIENT_SECRET + webhookSecret: $VAULT_GITHUB_APP_WEBHOOK_SECRET + privateKey: $VAULT_GITHUB_APP_PRIVATE_KEY diff --git a/workspaces/backstage/e2e-tests/tests/config/github-discovery/dynamic-plugins.yaml b/workspaces/backstage/e2e-tests/tests/config/github-discovery/dynamic-plugins.yaml new file mode 100644 index 000000000..5219a5721 --- /dev/null +++ b/workspaces/backstage/e2e-tests/tests/config/github-discovery/dynamic-plugins.yaml @@ -0,0 +1,3 @@ +plugins: + - package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-plugin-catalog-backend-module-github:bs_1.45.3__0.11.2 + disabled: false diff --git a/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml b/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml new file mode 100644 index 000000000..aa090a8e0 --- /dev/null +++ b/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Secret +metadata: + name: rhdh-secrets +type: Opaque +stringData: + VAULT_GITHUB_APP_APP_ID: $VAULT_GITHUB_APP_APP_ID + VAULT_GITHUB_APP_CLIENT_ID: $VAULT_GITHUB_APP_CLIENT_ID + VAULT_GITHUB_APP_CLIENT_SECRET: $VAULT_GITHUB_APP_CLIENT_SECRET + VAULT_GITHUB_APP_WEBHOOK_SECRET: $VAULT_GITHUB_APP_WEBHOOK_SECRET + VAULT_GITHUB_APP_PRIVATE_KEY: $VAULT_GITHUB_APP_PRIVATE_KEY + VAULT_GH_RHDH_QE_USER_TOKEN: $VAULT_GH_RHDH_QE_USER_TOKEN + GITHUB_TEST_ORGANIZATION: ${GITHUB_TEST_ORGANIZATION:-janus-qe} diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts new file mode 100644 index 000000000..f7b975c74 --- /dev/null +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -0,0 +1,61 @@ +import { CatalogPage } from "@red-hat-developer-hub/e2e-test-utils/pages"; +import { expect, test } from "@red-hat-developer-hub/e2e-test-utils/test"; +import GithubApiHelper from "../../support/api/github-helper"; +import { requireEnv } from "../../support/utils/require-env"; + +test.describe("Github Discovery Catalog", () => { + let catalogPage: CatalogPage; + let githubApiHelper: GithubApiHelper; + + test.beforeAll(async ({ rhdh }) => { + requireEnv("GITHUB_TEST_ORGANIZATION", "VAULT_GH_RHDH_QE_USER_TOKEN"); + + test.info().annotations.push({ + type: "component", + description: "api", + }); + + await rhdh.configure({ + auth: "github", + appConfig: "tests/config/github-discovery/app-config-rhdh.yaml", + secrets: "tests/config/github-discovery/rhdh-secrets.yaml", + dynamicPlugins: "tests/config/github-discovery/dynamic-plugins.yaml", + }); + await rhdh.deploy(); + }); + + test.beforeEach(async ({ loginHelper, page }) => { + await loginHelper.loginAsGithubUser(); + catalogPage = new CatalogPage(page); + githubApiHelper = new GithubApiHelper(); + await catalogPage.go(); + }); + + test(`Discover Organization's Catalog`, async () => { + const organizationRepos = await githubApiHelper.getReposFromOrg( + process.env.GITHUB_TEST_ORGANIZATION!, + ); + const reposNames: string[] = (organizationRepos as Array<{ name?: string }>) + .map((repo) => repo.name) + .filter((name): name is string => typeof name === "string"); + + const reposWithCatalogInfo: string[] = ( + await Promise.all( + reposNames.map(async (repo) => + (await githubApiHelper.fileExistsOnRepo( + `${process.env.GITHUB_TEST_ORGANIZATION!}/${repo}`, + "catalog-info.yaml", + )) + ? repo + : null, + ), + ) + ).filter((repo): repo is string => typeof repo === "string"); + + for (const repo of reposWithCatalogInfo) { + await catalogPage.search(repo); + const row = await catalogPage.tableRow(repo); + await expect(row).toBeVisible(); + } + }); +}); From 6d28072fb86fa71cc8ac10850d504a874dd84862 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Mon, 23 Mar 2026 12:50:08 +0100 Subject: [PATCH 02/25] Refactor github-helper Signed-off-by: Dominika Zemanovicova --- .../support/api/github-api-helper.ts | 97 +++++++++++++++++++ .../e2e-tests/support/api/github-helper.ts | 80 --------------- .../support/api/github-structures.ts | 3 - .../tests/specs/github-discovery.spec.ts | 4 +- 4 files changed, 99 insertions(+), 85 deletions(-) create mode 100644 workspaces/backstage/e2e-tests/support/api/github-api-helper.ts delete mode 100644 workspaces/backstage/e2e-tests/support/api/github-helper.ts delete mode 100644 workspaces/backstage/e2e-tests/support/api/github-structures.ts diff --git a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts new file mode 100644 index 000000000..9de48f568 --- /dev/null +++ b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts @@ -0,0 +1,97 @@ +import { APIResponse, request } from "@playwright/test"; +import { RHDH_GITHUB_TEST_ORGANIZATION } from "../constants/github/organization.js"; + +// https://docs.github.com/en/rest?apiVersion=2022-11-28 +export class GithubApiHelper { + private static githubApiVersion = "2022-11-28"; + private readonly defaultOrganization = + process.env.GITHUB_TEST_ORGANIZATION ?? RHDH_GITHUB_TEST_ORGANIZATION; + private readonly apiUrl = "https://api.github.com"; + + static async githubRequest( + method: string, + url: string, + body?: string | object, + ): Promise { + const context = await request.newContext(); + const options: { + method: string; + headers: Record; + data?: string | object; + } = { + method: method, + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${process.env.VAULT_GH_RHDH_QE_USER_TOKEN}`, + "X-GitHub-Api-Version": this.githubApiVersion, + }, + }; + + if (body !== undefined) { + options.data = body; + } + + const resp = await context.fetch(url, options); + if (!resp.ok()) { + throw new Error( + `Failed to ${method} ${url}: ${resp.status()} ${resp.statusText()}`, + ); + } + + return resp; + } + + static async getGithubPaginatedRequest( + url: string, + pageNo = 1, + response: unknown[] = [], + ): Promise { + const fullUrl = url.includes("?") + ? `${url}&page=${pageNo}` + : `${url}?page=${pageNo}`; + const result = await GithubApiHelper.githubRequest("GET", fullUrl); + const body = await result.json(); + + if (!Array.isArray(body)) { + throw new Error( + `Expected array but got ${typeof body}: ${JSON.stringify(body)}`, + ); + } + + if (body.length === 0) { + return response; + } + + return GithubApiHelper.getGithubPaginatedRequest(url, pageNo + 1, [ + ...response, + ...body, + ]); + } + + public async getOrganizationReposUrl( + org = this.defaultOrganization, + ): Promise { + const response = await GithubApiHelper.githubRequest( + "GET", + `${this.apiUrl}/orgs/${org}`, + ); + return (await response.json())["repos_url"]; + } + + public async getReposFromOrg(org = this.defaultOrganization) { + const reposUrl = await this.getOrganizationReposUrl(org); + // GitHub defaults to 30; use 100 to reduce API calls. + return GithubApiHelper.getGithubPaginatedRequest( + `${reposUrl}?per_page=100`, + ); + } + + public async fileExistsInRepo(repo: string, file: string): Promise { + const resp = await GithubApiHelper.githubRequest( + "GET", + `${this.apiUrl}/repos/${repo}/contents/${file}`, + ); + const status = resp.status(); + return [200, 302, 304].includes(status); + } +} diff --git a/workspaces/backstage/e2e-tests/support/api/github-helper.ts b/workspaces/backstage/e2e-tests/support/api/github-helper.ts deleted file mode 100644 index 257339d2e..000000000 --- a/workspaces/backstage/e2e-tests/support/api/github-helper.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { APIResponse, request } from "@playwright/test"; -import { GetOrganizationResponse } from "./github-structures"; -import { RHDH_GITHUB_TEST_ORGANIZATION } from "../constants/github/organization"; - -// https://docs.github.com/en/rest?apiVersion=2022-11-28 -export class GithubApiHelper { - private readonly apiUrl = "https://api.github.com"; - private readonly apiVersion = "2022-11-28"; - private readonly authHeader = { - Accept: "application/vnd.github+json", - Authorization: `Bearer ${process.env.VAULT_GH_RHDH_QE_USER_TOKEN}`, - "X-GitHub-Api-Version": this.apiVersion, - }; - - public async getOrganization( - org = process.env.GITHUB_TEST_ORGANIZATION ?? RHDH_GITHUB_TEST_ORGANIZATION, - ): Promise { - const req = await this._organization(org).get(); - const data = (await req.json()) as Record; - const reposUrl = data["repos_url"]; - if (typeof reposUrl !== "string" || reposUrl.length === 0) { - throw new Error( - "Invalid GitHub organization response: missing repos_url", - ); - } - return { reposUrl }; - } - - public async getReposFromOrg( - org = process.env.GITHUB_TEST_ORGANIZATION ?? RHDH_GITHUB_TEST_ORGANIZATION, - ) { - const req = await this._organization(org).repos(); - return req.json(); - } - - public async fileExistsOnRepo(repo: string, file: string): Promise { - const req = await this._repo(repo).getContent(file); - const status = req.status(); - if (status == 403) { - throw Error("You don-t have permissions to see this path"); - } - return [200, 302, 304].includes(status); - } - - private _myContext = request.newContext({ - baseURL: this.apiUrl, - extraHTTPHeaders: this.authHeader, - }); - - private _repo(repo: string) { - const url = `/repos/${repo}/`; - return { - getContent: async (path: string) => { - path = url + path; - const context = await this._myContext; - return context.get(path); - }, - }; - } - - private _organization(organization: string) { - const url = "/orgs/"; - - return { - get: async (): Promise => { - const path: string = url + organization; - const context = await this._myContext; - return context.get(path); - }, - - repos: async (): Promise => { - const context = await this._myContext; - const organizationResponse = await new GithubApiHelper() - ._organization(organization) - .get(); - return context.get((await organizationResponse.json()).repos_url); - }, - }; - } -} diff --git a/workspaces/backstage/e2e-tests/support/api/github-structures.ts b/workspaces/backstage/e2e-tests/support/api/github-structures.ts deleted file mode 100644 index 3b235a037..000000000 --- a/workspaces/backstage/e2e-tests/support/api/github-structures.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type GetOrganizationResponse = { - reposUrl: string; -}; diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index f7b975c74..5b7361937 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -1,7 +1,7 @@ import { CatalogPage } from "@red-hat-developer-hub/e2e-test-utils/pages"; import { expect, test } from "@red-hat-developer-hub/e2e-test-utils/test"; -import GithubApiHelper from "../../support/api/github-helper"; import { requireEnv } from "../../support/utils/require-env"; +import { GithubApiHelper } from "../../support/api/github-api-helper"; test.describe("Github Discovery Catalog", () => { let catalogPage: CatalogPage; @@ -42,7 +42,7 @@ test.describe("Github Discovery Catalog", () => { const reposWithCatalogInfo: string[] = ( await Promise.all( reposNames.map(async (repo) => - (await githubApiHelper.fileExistsOnRepo( + (await githubApiHelper.fileExistsInRepo( `${process.env.GITHUB_TEST_ORGANIZATION!}/${repo}`, "catalog-info.yaml", )) From acd3cc342c362d404335ee38c39f4bf0c0454c28 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Mon, 23 Mar 2026 17:23:00 +0100 Subject: [PATCH 03/25] Add failing to test Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/tests/specs/github-discovery.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index 5b7361937..3b3d43a27 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -57,5 +57,6 @@ test.describe("Github Discovery Catalog", () => { const row = await catalogPage.tableRow(repo); await expect(row).toBeVisible(); } + expect(true).toBe(false); }); }); From 7851ae2b6f509875b55ea98568ac39e1a6680989 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Mon, 23 Mar 2026 17:31:10 +0100 Subject: [PATCH 04/25] Update initialDelay for now Signed-off-by: Dominika Zemanovicova --- .../tests/config/github-discovery/app-config-rhdh.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml b/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml index d13079157..ad4af5f3e 100644 --- a/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml +++ b/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml @@ -23,7 +23,7 @@ catalog: frequency: hours: 24 initialDelay: - seconds: 15 + seconds: 3 timeout: minutes: 10 From 5e9019b7616b3ba048d5ab1b03fd3ef8d539f626 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 24 Mar 2026 08:54:00 +0100 Subject: [PATCH 05/25] Update test org Signed-off-by: Dominika Zemanovicova --- .../e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml b/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml index aa090a8e0..44f10f4f9 100644 --- a/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml +++ b/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml @@ -10,4 +10,4 @@ stringData: VAULT_GITHUB_APP_WEBHOOK_SECRET: $VAULT_GITHUB_APP_WEBHOOK_SECRET VAULT_GITHUB_APP_PRIVATE_KEY: $VAULT_GITHUB_APP_PRIVATE_KEY VAULT_GH_RHDH_QE_USER_TOKEN: $VAULT_GH_RHDH_QE_USER_TOKEN - GITHUB_TEST_ORGANIZATION: ${GITHUB_TEST_ORGANIZATION:-janus-qe} + GITHUB_TEST_ORGANIZATION: janus-qe From 0acabfaa03d63fa97573561f950b92aebad48bf7 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 24 Mar 2026 08:57:48 +0100 Subject: [PATCH 06/25] Fix maintainability issues Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/support/api/github-api-helper.ts | 4 ++-- .../backstage/e2e-tests/tests/specs/github-discovery.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts index 9de48f568..f7cd4fb1e 100644 --- a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts +++ b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts @@ -3,7 +3,7 @@ import { RHDH_GITHUB_TEST_ORGANIZATION } from "../constants/github/organization. // https://docs.github.com/en/rest?apiVersion=2022-11-28 export class GithubApiHelper { - private static githubApiVersion = "2022-11-28"; + private static readonly githubApiVersion = "2022-11-28"; private readonly defaultOrganization = process.env.GITHUB_TEST_ORGANIZATION ?? RHDH_GITHUB_TEST_ORGANIZATION; private readonly apiUrl = "https://api.github.com"; @@ -53,7 +53,7 @@ export class GithubApiHelper { const body = await result.json(); if (!Array.isArray(body)) { - throw new Error( + throw new TypeError( `Expected array but got ${typeof body}: ${JSON.stringify(body)}`, ); } diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index 3b3d43a27..e8617f603 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -33,7 +33,7 @@ test.describe("Github Discovery Catalog", () => { test(`Discover Organization's Catalog`, async () => { const organizationRepos = await githubApiHelper.getReposFromOrg( - process.env.GITHUB_TEST_ORGANIZATION!, + process.env.GITHUB_TEST_ORGANIZATION, ); const reposNames: string[] = (organizationRepos as Array<{ name?: string }>) .map((repo) => repo.name) From ffde60ddf1d7a369e4678b5a53e2b528be6b9777 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 24 Mar 2026 09:45:03 +0100 Subject: [PATCH 07/25] Unify org handling with other plugins Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/support/api/github-api-helper.ts | 6 ++---- .../tests/config/github-discovery/app-config-rhdh.yaml | 2 +- .../tests/config/github-discovery/rhdh-secrets.yaml | 2 +- .../e2e-tests/tests/specs/github-discovery.spec.ts | 7 ++++--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts index f7cd4fb1e..c2d7c2507 100644 --- a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts +++ b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts @@ -4,8 +4,6 @@ import { RHDH_GITHUB_TEST_ORGANIZATION } from "../constants/github/organization. // https://docs.github.com/en/rest?apiVersion=2022-11-28 export class GithubApiHelper { private static readonly githubApiVersion = "2022-11-28"; - private readonly defaultOrganization = - process.env.GITHUB_TEST_ORGANIZATION ?? RHDH_GITHUB_TEST_ORGANIZATION; private readonly apiUrl = "https://api.github.com"; static async githubRequest( @@ -69,7 +67,7 @@ export class GithubApiHelper { } public async getOrganizationReposUrl( - org = this.defaultOrganization, + org = RHDH_GITHUB_TEST_ORGANIZATION, ): Promise { const response = await GithubApiHelper.githubRequest( "GET", @@ -78,7 +76,7 @@ export class GithubApiHelper { return (await response.json())["repos_url"]; } - public async getReposFromOrg(org = this.defaultOrganization) { + public async getReposFromOrg(org = RHDH_GITHUB_TEST_ORGANIZATION) { const reposUrl = await this.getOrganizationReposUrl(org); // GitHub defaults to 30; use 100 to reduce API calls. return GithubApiHelper.getGithubPaginatedRequest( diff --git a/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml b/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml index ad4af5f3e..8c914044b 100644 --- a/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml +++ b/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml @@ -18,7 +18,7 @@ catalog: providers: github: providerId: - organization: $GITHUB_TEST_ORGANIZATION + organization: $GITHUB_ORG schedule: frequency: hours: 24 diff --git a/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml b/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml index 44f10f4f9..25d6e245b 100644 --- a/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml +++ b/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml @@ -10,4 +10,4 @@ stringData: VAULT_GITHUB_APP_WEBHOOK_SECRET: $VAULT_GITHUB_APP_WEBHOOK_SECRET VAULT_GITHUB_APP_PRIVATE_KEY: $VAULT_GITHUB_APP_PRIVATE_KEY VAULT_GH_RHDH_QE_USER_TOKEN: $VAULT_GH_RHDH_QE_USER_TOKEN - GITHUB_TEST_ORGANIZATION: janus-qe + GITHUB_ORG: janus-qe diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index e8617f603..8fa08d98b 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -2,13 +2,14 @@ import { CatalogPage } from "@red-hat-developer-hub/e2e-test-utils/pages"; import { expect, test } from "@red-hat-developer-hub/e2e-test-utils/test"; import { requireEnv } from "../../support/utils/require-env"; import { GithubApiHelper } from "../../support/api/github-api-helper"; +import { RHDH_GITHUB_TEST_ORGANIZATION } from "../../support/constants/github/organization"; test.describe("Github Discovery Catalog", () => { let catalogPage: CatalogPage; let githubApiHelper: GithubApiHelper; test.beforeAll(async ({ rhdh }) => { - requireEnv("GITHUB_TEST_ORGANIZATION", "VAULT_GH_RHDH_QE_USER_TOKEN"); + requireEnv("VAULT_GH_RHDH_QE_USER_TOKEN"); test.info().annotations.push({ type: "component", @@ -33,7 +34,7 @@ test.describe("Github Discovery Catalog", () => { test(`Discover Organization's Catalog`, async () => { const organizationRepos = await githubApiHelper.getReposFromOrg( - process.env.GITHUB_TEST_ORGANIZATION, + RHDH_GITHUB_TEST_ORGANIZATION, ); const reposNames: string[] = (organizationRepos as Array<{ name?: string }>) .map((repo) => repo.name) @@ -43,7 +44,7 @@ test.describe("Github Discovery Catalog", () => { await Promise.all( reposNames.map(async (repo) => (await githubApiHelper.fileExistsInRepo( - `${process.env.GITHUB_TEST_ORGANIZATION!}/${repo}`, + `${RHDH_GITHUB_TEST_ORGANIZATION}/${repo}`, "catalog-info.yaml", )) ? repo From 5548b87187ee2d31dc75aa65f45baa0562cbbb27 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 24 Mar 2026 10:51:46 +0100 Subject: [PATCH 08/25] Update token name Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/support/api/github-api-helper.ts | 7 ++++++- .../tests/config/github-discovery/rhdh-secrets.yaml | 2 +- .../e2e-tests/tests/specs/github-discovery.spec.ts | 3 --- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts index c2d7c2507..9278812ed 100644 --- a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts +++ b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts @@ -1,11 +1,16 @@ import { APIResponse, request } from "@playwright/test"; import { RHDH_GITHUB_TEST_ORGANIZATION } from "../constants/github/organization.js"; +import { requireEnv } from "../utils/require-env.js"; // https://docs.github.com/en/rest?apiVersion=2022-11-28 export class GithubApiHelper { private static readonly githubApiVersion = "2022-11-28"; private readonly apiUrl = "https://api.github.com"; + public constructor() { + requireEnv("VAULT_GITHUB_USER_TOKEN"); + } + static async githubRequest( method: string, url: string, @@ -20,7 +25,7 @@ export class GithubApiHelper { method: method, headers: { Accept: "application/vnd.github+json", - Authorization: `Bearer ${process.env.VAULT_GH_RHDH_QE_USER_TOKEN}`, + Authorization: `Bearer ${process.env.VAULT_GITHUB_USER_TOKEN}`, "X-GitHub-Api-Version": this.githubApiVersion, }, }; diff --git a/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml b/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml index 25d6e245b..4e3267d1f 100644 --- a/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml +++ b/workspaces/backstage/e2e-tests/tests/config/github-discovery/rhdh-secrets.yaml @@ -9,5 +9,5 @@ stringData: VAULT_GITHUB_APP_CLIENT_SECRET: $VAULT_GITHUB_APP_CLIENT_SECRET VAULT_GITHUB_APP_WEBHOOK_SECRET: $VAULT_GITHUB_APP_WEBHOOK_SECRET VAULT_GITHUB_APP_PRIVATE_KEY: $VAULT_GITHUB_APP_PRIVATE_KEY - VAULT_GH_RHDH_QE_USER_TOKEN: $VAULT_GH_RHDH_QE_USER_TOKEN + VAULT_GITHUB_USER_TOKEN: $VAULT_GITHUB_USER_TOKEN GITHUB_ORG: janus-qe diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index 8fa08d98b..80f5f7e76 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -1,6 +1,5 @@ import { CatalogPage } from "@red-hat-developer-hub/e2e-test-utils/pages"; import { expect, test } from "@red-hat-developer-hub/e2e-test-utils/test"; -import { requireEnv } from "../../support/utils/require-env"; import { GithubApiHelper } from "../../support/api/github-api-helper"; import { RHDH_GITHUB_TEST_ORGANIZATION } from "../../support/constants/github/organization"; @@ -9,8 +8,6 @@ test.describe("Github Discovery Catalog", () => { let githubApiHelper: GithubApiHelper; test.beforeAll(async ({ rhdh }) => { - requireEnv("VAULT_GH_RHDH_QE_USER_TOKEN"); - test.info().annotations.push({ type: "component", description: "api", From e5d17f06a402f87cd3d0ebd6d80b98b347fb5dab Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 24 Mar 2026 15:09:27 +0100 Subject: [PATCH 09/25] Update vars Signed-off-by: Dominika Zemanovicova --- .../config/github-discovery/app-config-rhdh.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml b/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml index 8c914044b..81ac010a5 100644 --- a/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml +++ b/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml @@ -18,7 +18,7 @@ catalog: providers: github: providerId: - organization: $GITHUB_ORG + organization: ${GITHUB_ORG} schedule: frequency: hours: 24 @@ -31,8 +31,8 @@ integrations: github: - host: github.com apps: - - appId: $VAULT_GITHUB_APP_APP_ID - clientId: $VAULT_GITHUB_APP_CLIENT_ID - clientSecret: $VAULT_GITHUB_APP_CLIENT_SECRET - webhookSecret: $VAULT_GITHUB_APP_WEBHOOK_SECRET - privateKey: $VAULT_GITHUB_APP_PRIVATE_KEY + - appId: ${VAULT_GITHUB_APP_APP_ID} + clientId: ${VAULT_GITHUB_APP_CLIENT_ID} + clientSecret: ${VAULT_GITHUB_APP_CLIENT_SECRET} + webhookSecret: ${VAULT_GITHUB_APP_WEBHOOK_SECRET} + privateKey: ${VAULT_GITHUB_APP_PRIVATE_KEY} From 0e210bc80b7c8a86af590320f708237f34c58873 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 24 Mar 2026 15:32:20 +0100 Subject: [PATCH 10/25] Add suppressError Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/support/api/github-api-helper.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts index 9278812ed..365b136c9 100644 --- a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts +++ b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts @@ -15,6 +15,7 @@ export class GithubApiHelper { method: string, url: string, body?: string | object, + suppressError: boolean = false, ): Promise { const context = await request.newContext(); const options: { @@ -35,7 +36,7 @@ export class GithubApiHelper { } const resp = await context.fetch(url, options); - if (!resp.ok()) { + if (!suppressError && !resp.ok()) { throw new Error( `Failed to ${method} ${url}: ${resp.status()} ${resp.statusText()}`, ); @@ -93,6 +94,8 @@ export class GithubApiHelper { const resp = await GithubApiHelper.githubRequest( "GET", `${this.apiUrl}/repos/${repo}/contents/${file}`, + undefined, + true, ); const status = resp.status(); return [200, 302, 304].includes(status); From 6417733dedd208f1f2c897ddd35164a18aae6ebe Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 24 Mar 2026 16:58:33 +0100 Subject: [PATCH 11/25] Update timeout and schedule Signed-off-by: Dominika Zemanovicova --- .../tests/config/github-discovery/app-config-rhdh.yaml | 2 -- .../backstage/e2e-tests/tests/specs/github-discovery.spec.ts | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml b/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml index 81ac010a5..d14a247ca 100644 --- a/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml +++ b/workspaces/backstage/e2e-tests/tests/config/github-discovery/app-config-rhdh.yaml @@ -22,8 +22,6 @@ catalog: schedule: frequency: hours: 24 - initialDelay: - seconds: 3 timeout: minutes: 10 diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index 80f5f7e76..bb4a6565d 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -8,6 +8,9 @@ test.describe("Github Discovery Catalog", () => { let githubApiHelper: GithubApiHelper; test.beforeAll(async ({ rhdh }) => { + // Allow time for deployment + 1 min provider refresh delay + browser setup + test.setTimeout(10 * 60 * 1000); + test.info().annotations.push({ type: "component", description: "api", @@ -20,6 +23,8 @@ test.describe("Github Discovery Catalog", () => { dynamicPlugins: "tests/config/github-discovery/dynamic-plugins.yaml", }); await rhdh.deploy(); + // Wait 1 minute for github provider to refresh entities before running tests + await new Promise((resolve) => setTimeout(resolve, 1 * 60 * 1000)); }); test.beforeEach(async ({ loginHelper, page }) => { From c96bb655d392d583edb5e3c6a500e10a960e37f6 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 24 Mar 2026 16:59:00 +0100 Subject: [PATCH 12/25] Test subset with same names Signed-off-by: Dominika Zemanovicova --- .../e2e-tests/tests/specs/github-discovery.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index bb4a6565d..d0cde9c9d 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -40,7 +40,9 @@ test.describe("Github Discovery Catalog", () => { ); const reposNames: string[] = (organizationRepos as Array<{ name?: string }>) .map((repo) => repo.name) - .filter((name): name is string => typeof name === "string"); + .filter((name): name is string => typeof name === "string") + // filter for subset of organization repositories where the repository name matches the entity name + .filter((name) => name.startsWith("test-annotator")); const reposWithCatalogInfo: string[] = ( await Promise.all( @@ -55,6 +57,8 @@ test.describe("Github Discovery Catalog", () => { ) ).filter((repo): repo is string => typeof repo === "string"); + expect(reposWithCatalogInfo.length).toBeGreaterThan(0); + for (const repo of reposWithCatalogInfo) { await catalogPage.search(repo); const row = await catalogPage.tableRow(repo); From ea7ccd23d1e8983601494daba3ec432cd37d9420 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 24 Mar 2026 17:00:26 +0100 Subject: [PATCH 13/25] Add fix for env variable Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/tests/specs/github-discovery.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index d0cde9c9d..15b480911 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -23,6 +23,8 @@ test.describe("Github Discovery Catalog", () => { dynamicPlugins: "tests/config/github-discovery/dynamic-plugins.yaml", }); await rhdh.deploy(); + // Remove after https://github.com/redhat-developer/rhdh-e2e-test-utils/pull/63 is merged + process.env.BASE_URL = process.env.RHDH_BASE_URL; // Wait 1 minute for github provider to refresh entities before running tests await new Promise((resolve) => setTimeout(resolve, 1 * 60 * 1000)); }); From 420af77ee6d6abc68ddb73fd723b84a81700b4d6 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 25 Mar 2026 10:36:11 +0100 Subject: [PATCH 14/25] Remove testing line Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/tests/specs/github-discovery.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index 15b480911..629ce409c 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -66,6 +66,5 @@ test.describe("Github Discovery Catalog", () => { const row = await catalogPage.tableRow(repo); await expect(row).toBeVisible(); } - expect(true).toBe(false); }); }); From 216c7d7d3425cee6abb6f94099c36235f798eb4c Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 25 Mar 2026 10:42:01 +0100 Subject: [PATCH 15/25] Update urils version Signed-off-by: Dominika Zemanovicova --- workspaces/backstage/e2e-tests/package.json | 2 +- .../e2e-tests/support/api/github-api-helper.ts | 2 +- .../backstage/e2e-tests/support/utils/require-env.ts | 8 -------- .../e2e-tests/tests/specs/github-discovery.spec.ts | 2 -- .../e2e-tests/tests/specs/github-events-module.spec.ts | 2 +- .../e2e-tests/tests/specs/kubernetes-rbac.spec.ts | 2 +- workspaces/backstage/e2e-tests/yarn.lock | 10 +++++----- 7 files changed, 9 insertions(+), 19 deletions(-) delete mode 100644 workspaces/backstage/e2e-tests/support/utils/require-env.ts diff --git a/workspaces/backstage/e2e-tests/package.json b/workspaces/backstage/e2e-tests/package.json index 0b41d117e..bd86f1de9 100644 --- a/workspaces/backstage/e2e-tests/package.json +++ b/workspaces/backstage/e2e-tests/package.json @@ -24,7 +24,7 @@ "devDependencies": { "@eslint/js": "^9.39.2", "@playwright/test": "1.57.0", - "@red-hat-developer-hub/e2e-test-utils": "1.1.15", + "@red-hat-developer-hub/e2e-test-utils": "1.1.21", "@types/node": "^24.10.1", "dotenv": "^16.4.7", "eslint": "^9.39.2", diff --git a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts index 365b136c9..c312ff2fc 100644 --- a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts +++ b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts @@ -1,6 +1,6 @@ import { APIResponse, request } from "@playwright/test"; import { RHDH_GITHUB_TEST_ORGANIZATION } from "../constants/github/organization.js"; -import { requireEnv } from "../utils/require-env.js"; +import { requireEnv } from "@red-hat-developer-hub/e2e-test-utils/utils"; // https://docs.github.com/en/rest?apiVersion=2022-11-28 export class GithubApiHelper { diff --git a/workspaces/backstage/e2e-tests/support/utils/require-env.ts b/workspaces/backstage/e2e-tests/support/utils/require-env.ts deleted file mode 100644 index e232b28cb..000000000 --- a/workspaces/backstage/e2e-tests/support/utils/require-env.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function requireEnv(...names: [string, ...string[]]): void { - for (const name of names) { - const value = process.env[name]; - if (!value) { - throw new Error(`${name} environment variable is required`); - } - } -} diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index 629ce409c..058fa89fa 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -23,8 +23,6 @@ test.describe("Github Discovery Catalog", () => { dynamicPlugins: "tests/config/github-discovery/dynamic-plugins.yaml", }); await rhdh.deploy(); - // Remove after https://github.com/redhat-developer/rhdh-e2e-test-utils/pull/63 is merged - process.env.BASE_URL = process.env.RHDH_BASE_URL; // Wait 1 minute for github provider to refresh entities before running tests await new Promise((resolve) => setTimeout(resolve, 1 * 60 * 1000)); }); diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts index 59fb1f1a7..7ae277370 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts @@ -7,7 +7,7 @@ import { import { createHmac } from "node:crypto"; import { CustomAPIHelper } from "../../support/api/api-helper"; import { GitHubEventsHelper } from "../../support/api/github-events"; -import { requireEnv } from "../../support/utils/require-env"; +import { requireEnv } from "@red-hat-developer-hub/e2e-test-utils/utils"; test.describe("GitHub Events Module", () => { let githubEventsHelper: GitHubEventsHelper; diff --git a/workspaces/backstage/e2e-tests/tests/specs/kubernetes-rbac.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/kubernetes-rbac.spec.ts index e4b3b5e5e..d4b345d2d 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/kubernetes-rbac.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/kubernetes-rbac.spec.ts @@ -5,7 +5,7 @@ import { KUBERNETES_USERS } from "../../support/constants/kubernetes/users"; import { KubernetesPage } from "../../support/pages/kubernetes"; import { KUBERNETES_COMPONENTS } from "../../support/pages/kubernetes-po"; import { KeycloakHelper } from "@red-hat-developer-hub/e2e-test-utils/keycloak"; -import { requireEnv } from "../../support/utils/require-env"; +import { requireEnv } from "@red-hat-developer-hub/e2e-test-utils/utils"; const rbacConfigsPath = path.resolve( process.cwd(), diff --git a/workspaces/backstage/e2e-tests/yarn.lock b/workspaces/backstage/e2e-tests/yarn.lock index 313b38cb4..9a2661a42 100644 --- a/workspaces/backstage/e2e-tests/yarn.lock +++ b/workspaces/backstage/e2e-tests/yarn.lock @@ -303,9 +303,9 @@ __metadata: languageName: node linkType: hard -"@red-hat-developer-hub/e2e-test-utils@npm:1.1.15": - version: 1.1.15 - resolution: "@red-hat-developer-hub/e2e-test-utils@npm:1.1.15" +"@red-hat-developer-hub/e2e-test-utils@npm:1.1.21": + version: 1.1.21 + resolution: "@red-hat-developer-hub/e2e-test-utils@npm:1.1.21" dependencies: "@axe-core/playwright": ^4.11.0 "@backstage-community/plugin-rbac-common": 1.23.0 @@ -327,7 +327,7 @@ __metadata: zx: ^8.8.5 peerDependencies: "@playwright/test": ^1.57.0 - checksum: 9b92ffbec69b77f263c875a9f3edb672fbe165f3cee8f84c5e2e7b9dd2428b693a2eb9b3375f42c0706960dd7ea2d434e7917f2eb5b6a3762d8f80c18a936c70 + checksum: d7c2d659eb806681a5f3f7b8812f7979e46d98a6c8f486dd2c2c24ddf25cf9b4cf12e4cd7f7070d74813483e53f51cd376152a665f66471a895b59195bf2ee62 languageName: node linkType: hard @@ -630,7 +630,7 @@ __metadata: dependencies: "@eslint/js": ^9.39.2 "@playwright/test": 1.57.0 - "@red-hat-developer-hub/e2e-test-utils": 1.1.15 + "@red-hat-developer-hub/e2e-test-utils": 1.1.21 "@types/node": ^24.10.1 dotenv: ^16.4.7 eslint: ^9.39.2 From da8f8305a31bb292533ed0727f696727f174e9f0 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 25 Mar 2026 11:24:43 +0100 Subject: [PATCH 16/25] Organize imports Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/tests/specs/kubernetes-rbac.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/workspaces/backstage/e2e-tests/tests/specs/kubernetes-rbac.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/kubernetes-rbac.spec.ts index d4b345d2d..731880d85 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/kubernetes-rbac.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/kubernetes-rbac.spec.ts @@ -1,11 +1,10 @@ +import { KeycloakHelper } from "@red-hat-developer-hub/e2e-test-utils/keycloak"; import { expect, test } from "@red-hat-developer-hub/e2e-test-utils/test"; -import { $ } from "@red-hat-developer-hub/e2e-test-utils/utils"; +import { $, requireEnv } from "@red-hat-developer-hub/e2e-test-utils/utils"; import * as path from "node:path"; import { KUBERNETES_USERS } from "../../support/constants/kubernetes/users"; import { KubernetesPage } from "../../support/pages/kubernetes"; import { KUBERNETES_COMPONENTS } from "../../support/pages/kubernetes-po"; -import { KeycloakHelper } from "@red-hat-developer-hub/e2e-test-utils/keycloak"; -import { requireEnv } from "@red-hat-developer-hub/e2e-test-utils/utils"; const rbacConfigsPath = path.resolve( process.cwd(), From 0bb07a8b1d86116493ab6255fe82082173c91bf1 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 25 Mar 2026 11:47:22 +0100 Subject: [PATCH 17/25] Split api helper Signed-off-by: Dominika Zemanovicova --- .../e2e-tests/support/api/api-helper.ts | 366 ------------------ .../support/api/catalog-api-helper.ts | 55 +++ .../support/api/github-api-helper.ts | 207 +++++++++- .../tests/specs/github-discovery.spec.ts | 13 +- .../tests/specs/github-events-module.spec.ts | 72 +--- 5 files changed, 279 insertions(+), 434 deletions(-) delete mode 100644 workspaces/backstage/e2e-tests/support/api/api-helper.ts create mode 100644 workspaces/backstage/e2e-tests/support/api/catalog-api-helper.ts diff --git a/workspaces/backstage/e2e-tests/support/api/api-helper.ts b/workspaces/backstage/e2e-tests/support/api/api-helper.ts deleted file mode 100644 index c71404d2f..000000000 --- a/workspaces/backstage/e2e-tests/support/api/api-helper.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { request } from "@playwright/test"; -/** - * Helper class for making API calls to GitHub and RHDH - */ -export class CustomAPIHelper { - /** - * Get a group entity from the RHDH catalog API - */ - static async getGroupEntityFromAPI( - baseUrl: string, - token: string, - groupName: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise { - const context = await request.newContext({ - ignoreHTTPSErrors: true, - }); - - const url = `${baseUrl}/api/catalog/entities/by-name/group/default/${groupName}`; - const response = await context.get(url, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - if (!response.ok()) { - throw new Error( - `Failed to get group entity: ${response.status()} ${response.statusText()}`, - ); - } - - return await response.json(); - } - - /** - * Extract group members from a group entity - */ - static async getGroupMembers( - baseUrl: string, - token: string, - groupName: string, - ): Promise { - const groupEntity = await CustomAPIHelper.getGroupEntityFromAPI( - baseUrl, - token, - groupName, - ); - const members = - groupEntity.relations - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ?.filter((r: any) => r.type === "hasMember") - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .map((r: any) => r.targetRef.split("/")[1]) || []; - return members; - } - - /** - * Create a GitHub repository with a file - */ - static async createGitHubRepoWithFile( - owner: string, - repo: string, - filePath: string, - content: string, - token: string, - ): Promise { - const createRepoResponse = await fetch( - `https://api.github.com/orgs/${owner}/repos`, - { - method: "POST", - headers: { - Authorization: `token ${token}`, - "Content-Type": "application/json", - Accept: "application/vnd.github+json", - }, - body: JSON.stringify({ - name: repo, - private: false, - // eslint-disable-next-line @typescript-eslint/naming-convention - auto_init: true, - }), - }, - ); - - if (!createRepoResponse.ok) { - const errorText = await createRepoResponse.text(); - throw new Error( - `Failed to create repository: ${createRepoResponse.status} ${errorText}`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, 2000)); - - const createFileResponse = await fetch( - `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`, - { - method: "PUT", - headers: { - Authorization: `token ${token}`, - "Content-Type": "application/json", - Accept: "application/vnd.github+json", - }, - body: JSON.stringify({ - message: `Add ${filePath}`, - content: Buffer.from(content).toString("base64"), - }), - }, - ); - - if (!createFileResponse.ok) { - const errorText = await createFileResponse.text(); - throw new Error( - `Failed to create file: ${createFileResponse.status} ${errorText}`, - ); - } - } - - /** - * Update a file in a GitHub repository - */ - static async updateFileInRepo( - owner: string, - repo: string, - filePath: string, - content: string, - commitMessage: string, - token: string, - ): Promise { - const getFileResponse = await fetch( - `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`, - { - headers: { - Authorization: `token ${token}`, - Accept: "application/vnd.github+json", - }, - }, - ); - - if (!getFileResponse.ok) { - throw new Error( - `Failed to get file: ${getFileResponse.status} ${getFileResponse.statusText}`, - ); - } - - const fileData = (await getFileResponse.json()) as { sha: string }; - - const updateFileResponse = await fetch( - `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`, - { - method: "PUT", - headers: { - Authorization: `token ${token}`, - "Content-Type": "application/json", - Accept: "application/vnd.github+json", - }, - body: JSON.stringify({ - message: commitMessage, - content: Buffer.from(content).toString("base64"), - sha: fileData.sha, - }), - }, - ); - - if (!updateFileResponse.ok) { - const errorText = await updateFileResponse.text(); - throw new Error( - `Failed to update file: ${updateFileResponse.status} ${errorText}`, - ); - } - } - - /** - * Delete a file from a GitHub repository - */ - static async deleteFileInRepo( - owner: string, - repo: string, - filePath: string, - commitMessage: string, - token: string, - ): Promise { - const getFileResponse = await fetch( - `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`, - { - headers: { - Authorization: `token ${token}`, - Accept: "application/vnd.github+json", - }, - }, - ); - - if (getFileResponse.status === 404) { - console.log(`File ${filePath} already deleted or doesn't exist`); - return; - } - - if (!getFileResponse.ok) { - throw new Error( - `Failed to get file: ${getFileResponse.status} ${getFileResponse.statusText}`, - ); - } - - const fileData = (await getFileResponse.json()) as { sha: string }; - - const deleteFileResponse = await fetch( - `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`, - { - method: "DELETE", - headers: { - Authorization: `token ${token}`, - "Content-Type": "application/json", - Accept: "application/vnd.github+json", - }, - body: JSON.stringify({ - message: commitMessage, - sha: fileData.sha, - }), - }, - ); - - if (!deleteFileResponse.ok && deleteFileResponse.status !== 404) { - const errorText = await deleteFileResponse.text(); - throw new Error( - `Failed to delete file: ${deleteFileResponse.status} ${errorText}`, - ); - } - } - - /** - * Delete a GitHub repository - */ - static async deleteRepo( - owner: string, - repo: string, - token: string, - ): Promise { - const response = await fetch( - `https://api.github.com/repos/${owner}/${repo}`, - { - method: "DELETE", - headers: { - Authorization: `token ${token}`, - Accept: "application/vnd.github+json", - }, - }, - ); - - if (!response.ok && response.status !== 404) { - const errorText = await response.text(); - throw new Error( - `Failed to delete repository: ${response.status} ${errorText}`, - ); - } - } - - /** - * Create a team in a GitHub organization - */ - static async createTeamInOrg( - org: string, - teamName: string, - token: string, - ): Promise { - const response = await fetch(`https://api.github.com/orgs/${org}/teams`, { - method: "POST", - headers: { - Authorization: `token ${token}`, - "Content-Type": "application/json", - Accept: "application/vnd.github+json", - }, - body: JSON.stringify({ - name: teamName, - privacy: "closed", - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Failed to create team: ${response.status} ${errorText}`); - } - } - - /** - * Delete a team from a GitHub organization - */ - static async deleteTeamFromOrg( - org: string, - teamName: string, - token: string, - ): Promise { - const response = await fetch( - `https://api.github.com/orgs/${org}/teams/${teamName}`, - { - method: "DELETE", - headers: { - Authorization: `token ${token}`, - Accept: "application/vnd.github+json", - }, - }, - ); - - if (!response.ok && response.status !== 404) { - const errorText = await response.text(); - throw new Error(`Failed to delete team: ${response.status} ${errorText}`); - } - } - - /** - * Add a user to a team in a GitHub organization - */ - static async addUserToTeam( - org: string, - teamName: string, - username: string, - token: string, - ): Promise { - const response = await fetch( - `https://api.github.com/orgs/${org}/teams/${teamName}/memberships/${username}`, - { - method: "PUT", - headers: { - Authorization: `token ${token}`, - "Content-Type": "application/json", - Accept: "application/vnd.github+json", - }, - body: JSON.stringify({ - role: "member", - }), - }, - ); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error( - `Failed to add user to team: ${response.status} ${errorText}`, - ); - } - } - - /** - * Remove a user from a team in a GitHub organization - */ - static async removeUserFromTeam( - org: string, - teamName: string, - username: string, - token: string, - ): Promise { - const response = await fetch( - `https://api.github.com/orgs/${org}/teams/${teamName}/memberships/${username}`, - { - method: "DELETE", - headers: { - Authorization: `token ${token}`, - Accept: "application/vnd.github+json", - }, - }, - ); - - if (!response.ok && response.status !== 404) { - const errorText = await response.text(); - throw new Error( - `Failed to remove user from team: ${response.status} ${errorText}`, - ); - } - } -} diff --git a/workspaces/backstage/e2e-tests/support/api/catalog-api-helper.ts b/workspaces/backstage/e2e-tests/support/api/catalog-api-helper.ts new file mode 100644 index 000000000..5d1a92bee --- /dev/null +++ b/workspaces/backstage/e2e-tests/support/api/catalog-api-helper.ts @@ -0,0 +1,55 @@ +import { request } from "@playwright/test"; +/** + * Helper class for making API calls to Catalog + */ +export class CatalogApiHelper { + /** + * Get a group entity from the RHDH catalog API + */ + static async getGroupEntity( + baseUrl: string, + token: string, + groupName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + const context = await request.newContext({ + ignoreHTTPSErrors: true, + }); + + const url = `${baseUrl}/api/catalog/entities/by-name/group/default/${groupName}`; + const response = await context.get(url, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + if (!response.ok()) { + throw new Error( + `Failed to get group entity: ${response.status()} ${response.statusText()}`, + ); + } + + return await response.json(); + } + + /** + * Extract group members from a group entity + */ + static async getGroupMembers( + baseUrl: string, + token: string, + groupName: string, + ): Promise { + const groupEntity = await CatalogApiHelper.getGroupEntity( + baseUrl, + token, + groupName, + ); + const members = + groupEntity.relations + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ?.filter((r: any) => r.type === "hasMember") + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((r: any) => r.targetRef.split("/")[1]) || []; + return members; + } +} diff --git a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts index c312ff2fc..92c727e37 100644 --- a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts +++ b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts @@ -3,8 +3,8 @@ import { RHDH_GITHUB_TEST_ORGANIZATION } from "../constants/github/organization. import { requireEnv } from "@red-hat-developer-hub/e2e-test-utils/utils"; // https://docs.github.com/en/rest?apiVersion=2022-11-28 -export class GithubApiHelper { - private static readonly githubApiVersion = "2022-11-28"; +export class GitHubApiHelper { + private static readonly gitHubApiVersion = "2022-11-28"; private readonly apiUrl = "https://api.github.com"; public constructor() { @@ -27,7 +27,7 @@ export class GithubApiHelper { headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${process.env.VAULT_GITHUB_USER_TOKEN}`, - "X-GitHub-Api-Version": this.githubApiVersion, + "X-GitHub-Api-Version": this.gitHubApiVersion, }, }; @@ -53,7 +53,7 @@ export class GithubApiHelper { const fullUrl = url.includes("?") ? `${url}&page=${pageNo}` : `${url}?page=${pageNo}`; - const result = await GithubApiHelper.githubRequest("GET", fullUrl); + const result = await GitHubApiHelper.githubRequest("GET", fullUrl); const body = await result.json(); if (!Array.isArray(body)) { @@ -66,16 +66,199 @@ export class GithubApiHelper { return response; } - return GithubApiHelper.getGithubPaginatedRequest(url, pageNo + 1, [ + return GitHubApiHelper.getGithubPaginatedRequest(url, pageNo + 1, [ ...response, ...body, ]); } + /** + * Create a GitHub repository with a file + */ + public async createGitHubRepoWithFile( + owner: string, + repo: string, + filePath: string, + content: string, + ): Promise { + await GitHubApiHelper.githubRequest( + "POST", + `${this.apiUrl}/orgs/${owner}/repos`, + JSON.stringify({ + name: repo, + private: false, + // eslint-disable-next-line @typescript-eslint/naming-convention + auto_init: true, + }), + ); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + + await GitHubApiHelper.githubRequest( + "PUT", + `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, + JSON.stringify({ + message: `Add ${filePath}`, + content: Buffer.from(content).toString("base64"), + }), + ); + } + + /** + * Update a file in a GitHub repository + */ + public async updateFileInRepo( + owner: string, + repo: string, + filePath: string, + content: string, + commitMessage: string, + ): Promise { + const resp = await GitHubApiHelper.githubRequest( + "GET", + `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, + ); + const fileData = (await resp.json()) as { sha: string }; + await GitHubApiHelper.githubRequest( + "PUT", + `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, + JSON.stringify({ + message: commitMessage, + content: Buffer.from(content).toString("base64"), + sha: fileData.sha, + }), + ); + } + + /** + * Delete a file from a GitHub repository + */ + public async deleteFileInRepo( + owner: string, + repo: string, + filePath: string, + commitMessage: string, + ): Promise { + const getFileResponse = await GitHubApiHelper.githubRequest( + "GET", + `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, + undefined, + true, + ); + if (getFileResponse.status() === 404) { + return; + } + if (!getFileResponse.ok()) { + throw new Error( + `Failed to get file: ${getFileResponse.status()} ${getFileResponse.statusText()}`, + ); + } + + const fileData = (await getFileResponse.json()) as { sha: string }; + + await GitHubApiHelper.githubRequest( + "DELETE", + `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, + JSON.stringify({ + message: commitMessage, + sha: fileData.sha, + }), + ); + } + + /** + * Delete a GitHub repository + */ + public async deleteRepo(owner: string, repo: string): Promise { + const response = await GitHubApiHelper.githubRequest( + "DELETE", + `${this.apiUrl}/repos/${owner}/${repo}`, + undefined, + true, + ); + + if (!response.ok() && response.status() !== 404) { + throw new Error( + `Failed to delete repository: ${response.status()} ${response.statusText()}`, + ); + } + } + + /** + * Create a team in a GitHub organization + */ + public async createTeamInOrg(org: string, teamName: string): Promise { + await GitHubApiHelper.githubRequest( + "POST", + `${this.apiUrl}/orgs/${org}/teams`, + JSON.stringify({ + name: teamName, + privacy: "closed", + }), + ); + } + + /** + * Delete a team from a GitHub organization + */ + public async deleteTeamFromOrg(org: string, teamName: string): Promise { + const response = await GitHubApiHelper.githubRequest( + "DELETE", + `${this.apiUrl}/orgs/${org}/teams/${teamName}`, + undefined, + true, + ); + + if (!response.ok() && response.status() !== 404) { + throw new Error( + `Failed to delete team: ${response.status()} ${response.statusText()}`, + ); + } + } + + /** + * Add a user to a team in a GitHub organization + */ + public async addUserToTeam( + org: string, + teamName: string, + username: string, + ): Promise { + await GitHubApiHelper.githubRequest( + "PUT", + `${this.apiUrl}/orgs/${org}/teams/${teamName}/memberships/${username}`, + JSON.stringify({ + role: "member", + }), + ); + } + + /** + * Remove a user from a team in a GitHub organization + */ + public async removeUserFromTeam( + org: string, + teamName: string, + username: string, + ): Promise { + const response = await GitHubApiHelper.githubRequest( + "DELETE", + `${this.apiUrl}/orgs/${org}/teams/${teamName}/memberships/${username}`, + undefined, + true, + ); + + if (!response.ok() && response.status() !== 404) { + throw new Error( + `Failed to remove user from team: ${response.status()} ${response.statusText()}`, + ); + } + } + public async getOrganizationReposUrl( org = RHDH_GITHUB_TEST_ORGANIZATION, ): Promise { - const response = await GithubApiHelper.githubRequest( + const response = await GitHubApiHelper.githubRequest( "GET", `${this.apiUrl}/orgs/${org}`, ); @@ -85,15 +268,19 @@ export class GithubApiHelper { public async getReposFromOrg(org = RHDH_GITHUB_TEST_ORGANIZATION) { const reposUrl = await this.getOrganizationReposUrl(org); // GitHub defaults to 30; use 100 to reduce API calls. - return GithubApiHelper.getGithubPaginatedRequest( + return GitHubApiHelper.getGithubPaginatedRequest( `${reposUrl}?per_page=100`, ); } - public async fileExistsInRepo(repo: string, file: string): Promise { - const resp = await GithubApiHelper.githubRequest( + public async fileExistsInRepo( + owner: string, + repo: string, + filePath: string, + ): Promise { + const resp = await GitHubApiHelper.githubRequest( "GET", - `${this.apiUrl}/repos/${repo}/contents/${file}`, + `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, undefined, true, ); diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index 058fa89fa..72ae3c3e8 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -1,11 +1,11 @@ import { CatalogPage } from "@red-hat-developer-hub/e2e-test-utils/pages"; import { expect, test } from "@red-hat-developer-hub/e2e-test-utils/test"; -import { GithubApiHelper } from "../../support/api/github-api-helper"; +import { GitHubApiHelper } from "../../support/api/github-api-helper"; import { RHDH_GITHUB_TEST_ORGANIZATION } from "../../support/constants/github/organization"; test.describe("Github Discovery Catalog", () => { let catalogPage: CatalogPage; - let githubApiHelper: GithubApiHelper; + let gitHubApiHelper: GitHubApiHelper; test.beforeAll(async ({ rhdh }) => { // Allow time for deployment + 1 min provider refresh delay + browser setup @@ -30,12 +30,12 @@ test.describe("Github Discovery Catalog", () => { test.beforeEach(async ({ loginHelper, page }) => { await loginHelper.loginAsGithubUser(); catalogPage = new CatalogPage(page); - githubApiHelper = new GithubApiHelper(); + gitHubApiHelper = new GitHubApiHelper(); await catalogPage.go(); }); test(`Discover Organization's Catalog`, async () => { - const organizationRepos = await githubApiHelper.getReposFromOrg( + const organizationRepos = await gitHubApiHelper.getReposFromOrg( RHDH_GITHUB_TEST_ORGANIZATION, ); const reposNames: string[] = (organizationRepos as Array<{ name?: string }>) @@ -47,8 +47,9 @@ test.describe("Github Discovery Catalog", () => { const reposWithCatalogInfo: string[] = ( await Promise.all( reposNames.map(async (repo) => - (await githubApiHelper.fileExistsInRepo( - `${RHDH_GITHUB_TEST_ORGANIZATION}/${repo}`, + (await gitHubApiHelper.fileExistsInRepo( + RHDH_GITHUB_TEST_ORGANIZATION, + repo, "catalog-info.yaml", )) ? repo diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts index 7ae277370..5e5c00dc3 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts @@ -5,20 +5,19 @@ import { test, } from "@red-hat-developer-hub/e2e-test-utils/test"; import { createHmac } from "node:crypto"; -import { CustomAPIHelper } from "../../support/api/api-helper"; +import { CatalogApiHelper } from "../../support/api/catalog-api-helper"; import { GitHubEventsHelper } from "../../support/api/github-events"; import { requireEnv } from "@red-hat-developer-hub/e2e-test-utils/utils"; +import { GitHubApiHelper } from "../../support/api/github-api-helper"; test.describe("GitHub Events Module", () => { let githubEventsHelper: GitHubEventsHelper; + let githubApiHelper: GitHubApiHelper; let staticToken: string; let rhdhBaseUrl: string; test.beforeAll(async ({ rhdh }) => { - requireEnv( - "VAULT_GITHUB_APP_WEBHOOK_SECRET", - "VAULT_GH_RHDH_QE_USER_TOKEN", - ); + requireEnv("VAULT_GITHUB_APP_WEBHOOK_SECRET"); await rhdh.configure({ auth: "keycloak", @@ -33,6 +32,7 @@ test.describe("GitHub Events Module", () => { rhdh.rhdhUrl, process.env.VAULT_GITHUB_APP_WEBHOOK_SECRET!, ); + githubApiHelper = new GitHubApiHelper(); rhdhBaseUrl = rhdh.rhdhUrl; }); @@ -101,12 +101,11 @@ spec: lifecycle: unknown owner: user:default/janus-qe`; - await CustomAPIHelper.createGitHubRepoWithFile( + await githubApiHelper.createGitHubRepoWithFile( catalogRepoDetails.owner, catalogRepoDetails.name, "catalog-info.yaml", catalogInfoYamlContent, - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, ); await githubEventsHelper.sendPushEvent( @@ -147,13 +146,12 @@ spec: type: other lifecycle: unknown owner: user:default/janus-qe`; - await CustomAPIHelper.updateFileInRepo( + await githubApiHelper.updateFileInRepo( catalogRepoDetails.owner, catalogRepoDetails.name, "catalog-info.yaml", updatedCatalogInfoYaml, "Update catalog-info.yaml description", - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, ); await githubEventsHelper.sendPushEvent( `janus-qe/${catalogRepoName}`, @@ -183,12 +181,11 @@ spec: }); test("Deleting an entity from the catalog", async ({ page, uiHelper }) => { - await CustomAPIHelper.deleteFileInRepo( + await githubApiHelper.deleteFileInRepo( catalogRepoDetails.owner, catalogRepoDetails.name, "catalog-info.yaml", "Remove catalog-info.yaml", - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, ); await githubEventsHelper.sendPushEvent( `janus-qe/${catalogRepoName}`, @@ -216,10 +213,9 @@ spec: }); test.afterAll(async () => { - await CustomAPIHelper.deleteRepo( + await githubApiHelper.deleteRepo( catalogRepoDetails.owner, catalogRepoDetails.name, - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, ); }); }); @@ -230,11 +226,7 @@ spec: const teamName = "test-team-" + Date.now(); test("Adding a new group", async ({ page, uiHelper }) => { - await CustomAPIHelper.createTeamInOrg( - "janus-qe", - teamName, - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, - ); + await githubApiHelper.createTeamInOrg("janus-qe", teamName); await githubEventsHelper.sendTeamEvent("created", teamName, "janus-qe"); await expect @@ -258,11 +250,7 @@ spec: }); test("Deleting a group", async ({ page, uiHelper }) => { - await CustomAPIHelper.deleteTeamFromOrg( - "janus-qe", - teamName, - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, - ); + await githubApiHelper.deleteTeamFromOrg("janus-qe", teamName); await githubEventsHelper.sendTeamEvent("deleted", teamName, "janus-qe"); @@ -338,11 +326,7 @@ spec: teamName = "test-team-" + Date.now(); - await CustomAPIHelper.createTeamInOrg( - "janus-qe", - teamName, - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, - ); + await githubApiHelper.createTeamInOrg("janus-qe", teamName); teamCreated = true; await githubEventsHelper.sendTeamEvent("created", teamName, "janus-qe"); @@ -352,32 +336,22 @@ spec: test.afterEach(async () => { if (userAddedToTeam) { - await CustomAPIHelper.removeUserFromTeam( + await githubApiHelper.removeUserFromTeam( "janus-qe", teamName, "rhdh-qe", - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, ); userAddedToTeam = false; } if (teamCreated) { - await CustomAPIHelper.deleteTeamFromOrg( - "janus-qe", - teamName, - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, - ); + await githubApiHelper.deleteTeamFromOrg("janus-qe", teamName); teamCreated = false; } }); test("Adding a user to a group", async ({ uiHelper }) => { - await CustomAPIHelper.addUserToTeam( - "janus-qe", - teamName, - "rhdh-qe", - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, - ); + await githubApiHelper.addUserToTeam("janus-qe", teamName, "rhdh-qe"); userAddedToTeam = true; await githubEventsHelper.sendMembershipEvent( @@ -392,7 +366,7 @@ spec: await expect .poll( () => - CustomAPIHelper.getGroupMembers( + CatalogApiHelper.getGroupMembers( rhdhBaseUrl, staticToken, teamName, @@ -407,12 +381,7 @@ spec: }); test("Removing a user from a group", async ({ uiHelper }) => { - await CustomAPIHelper.addUserToTeam( - "janus-qe", - teamName, - "rhdh-qe", - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, - ); + await githubApiHelper.addUserToTeam("janus-qe", teamName, "rhdh-qe"); userAddedToTeam = true; await githubEventsHelper.sendMembershipEvent( @@ -425,7 +394,7 @@ spec: await expect .poll( () => - CustomAPIHelper.getGroupMembers( + CatalogApiHelper.getGroupMembers( rhdhBaseUrl, staticToken, teamName, @@ -438,11 +407,10 @@ spec: ) .toContain("rhdh-qe"); - await CustomAPIHelper.removeUserFromTeam( + await githubApiHelper.removeUserFromTeam( "janus-qe", teamName, "rhdh-qe", - process.env.VAULT_GH_RHDH_QE_USER_TOKEN!, ); userAddedToTeam = false; @@ -458,7 +426,7 @@ spec: await expect .poll( () => - CustomAPIHelper.getGroupMembers( + CatalogApiHelper.getGroupMembers( rhdhBaseUrl, staticToken, teamName, From f2cb12a6872433e23f33497218615774e8c83026 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 25 Mar 2026 13:59:14 +0100 Subject: [PATCH 18/25] Make sure subset is small Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/tests/specs/github-discovery.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index 72ae3c3e8..532fa9813 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -42,7 +42,8 @@ test.describe("Github Discovery Catalog", () => { .map((repo) => repo.name) .filter((name): name is string => typeof name === "string") // filter for subset of organization repositories where the repository name matches the entity name - .filter((name) => name.startsWith("test-annotator")); + .filter((name) => name.startsWith("test-annotator")) + .slice(0, 5); const reposWithCatalogInfo: string[] = ( await Promise.all( From c2afa906ea881379f8a1814f948676862ab3fc06 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 25 Mar 2026 15:44:19 +0100 Subject: [PATCH 19/25] Update to serial Signed-off-by: Dominika Zemanovicova --- .../e2e-tests/tests/specs/github-events-module.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts index 5e5c00dc3..9972649ad 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts @@ -79,7 +79,7 @@ test.describe("GitHub Events Module", () => { await context.dispose(); }); - test.describe("GitHub Discovery", () => { + test.describe.serial("GitHub Discovery", () => { const catalogRepoName = `janus-test-github-events-test-${Date.now()}`; const catalogRepoDetails = { name: catalogRepoName, From 58d9e17a881f8eb703ca54ce07ef59f51675701e Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Thu, 26 Mar 2026 15:43:01 +0100 Subject: [PATCH 20/25] Update utils version Signed-off-by: Dominika Zemanovicova --- workspaces/backstage/e2e-tests/package.json | 2 +- workspaces/backstage/e2e-tests/yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/workspaces/backstage/e2e-tests/package.json b/workspaces/backstage/e2e-tests/package.json index bd86f1de9..afb5ef2ae 100644 --- a/workspaces/backstage/e2e-tests/package.json +++ b/workspaces/backstage/e2e-tests/package.json @@ -24,7 +24,7 @@ "devDependencies": { "@eslint/js": "^9.39.2", "@playwright/test": "1.57.0", - "@red-hat-developer-hub/e2e-test-utils": "1.1.21", + "@red-hat-developer-hub/e2e-test-utils": "1.1.22", "@types/node": "^24.10.1", "dotenv": "^16.4.7", "eslint": "^9.39.2", diff --git a/workspaces/backstage/e2e-tests/yarn.lock b/workspaces/backstage/e2e-tests/yarn.lock index 9a2661a42..2ba07484d 100644 --- a/workspaces/backstage/e2e-tests/yarn.lock +++ b/workspaces/backstage/e2e-tests/yarn.lock @@ -303,9 +303,9 @@ __metadata: languageName: node linkType: hard -"@red-hat-developer-hub/e2e-test-utils@npm:1.1.21": - version: 1.1.21 - resolution: "@red-hat-developer-hub/e2e-test-utils@npm:1.1.21" +"@red-hat-developer-hub/e2e-test-utils@npm:1.1.22": + version: 1.1.22 + resolution: "@red-hat-developer-hub/e2e-test-utils@npm:1.1.22" dependencies: "@axe-core/playwright": ^4.11.0 "@backstage-community/plugin-rbac-common": 1.23.0 @@ -327,7 +327,7 @@ __metadata: zx: ^8.8.5 peerDependencies: "@playwright/test": ^1.57.0 - checksum: d7c2d659eb806681a5f3f7b8812f7979e46d98a6c8f486dd2c2c24ddf25cf9b4cf12e4cd7f7070d74813483e53f51cd376152a665f66471a895b59195bf2ee62 + checksum: 9f2b83a8a4ee7d1a0ccbbff2250ff75e229525acc0aa62d7d64c5889d6cffeadf105ae28bc54ae57744c9a5014d4850c24618871a32f4f14b15d30a726c85ec0 languageName: node linkType: hard @@ -630,7 +630,7 @@ __metadata: dependencies: "@eslint/js": ^9.39.2 "@playwright/test": 1.57.0 - "@red-hat-developer-hub/e2e-test-utils": 1.1.21 + "@red-hat-developer-hub/e2e-test-utils": 1.1.22 "@types/node": ^24.10.1 dotenv: ^16.4.7 eslint: ^9.39.2 From d2a386b9627db22f27d65e237b4dfe737d805655 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Mon, 30 Mar 2026 12:50:17 +0200 Subject: [PATCH 21/25] Login as guest Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/tests/specs/github-discovery.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index 532fa9813..f72632bd3 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -17,7 +17,7 @@ test.describe("Github Discovery Catalog", () => { }); await rhdh.configure({ - auth: "github", + auth: "guest", appConfig: "tests/config/github-discovery/app-config-rhdh.yaml", secrets: "tests/config/github-discovery/rhdh-secrets.yaml", dynamicPlugins: "tests/config/github-discovery/dynamic-plugins.yaml", @@ -28,7 +28,7 @@ test.describe("Github Discovery Catalog", () => { }); test.beforeEach(async ({ loginHelper, page }) => { - await loginHelper.loginAsGithubUser(); + await loginHelper.loginAsGuest(); catalogPage = new CatalogPage(page); gitHubApiHelper = new GitHubApiHelper(); await catalogPage.go(); From e6f35cc61e272f8a0edda31132955659f2d84b29 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Mon, 30 Mar 2026 18:51:02 +0200 Subject: [PATCH 22/25] Use directly ApiHelper Signed-off-by: Dominika Zemanovicova --- .../support/api/github-api-helper.ts | 207 +++++------------- .../tests/specs/github-discovery.spec.ts | 6 +- .../tests/specs/github-events-module.spec.ts | 26 +-- 3 files changed, 64 insertions(+), 175 deletions(-) diff --git a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts index 92c727e37..a4fed70df 100644 --- a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts +++ b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts @@ -1,127 +1,50 @@ -import { APIResponse, request } from "@playwright/test"; +import { APIResponse } from "@playwright/test"; import { RHDH_GITHUB_TEST_ORGANIZATION } from "../constants/github/organization.js"; -import { requireEnv } from "@red-hat-developer-hub/e2e-test-utils/utils"; +import { + APIHelper, + GITHUB_API_ENDPOINTS, +} from "@red-hat-developer-hub/e2e-test-utils/helpers"; -// https://docs.github.com/en/rest?apiVersion=2022-11-28 -export class GitHubApiHelper { - private static readonly gitHubApiVersion = "2022-11-28"; - private readonly apiUrl = "https://api.github.com"; - - public constructor() { - requireEnv("VAULT_GITHUB_USER_TOKEN"); - } +const baseApiUrl = "https://api.github.com"; +const getOrgUrl = (owner: string) => `${baseApiUrl}/orgs/${owner}`; - static async githubRequest( +// https://docs.github.com/en/rest?apiVersion=2022-11-28 +export class GitHubApiHelper extends APIHelper { + static async safeGithubRequest( method: string, url: string, body?: string | object, - suppressError: boolean = false, ): Promise { - const context = await request.newContext(); - const options: { - method: string; - headers: Record; - data?: string | object; - } = { - method: method, - headers: { - Accept: "application/vnd.github+json", - Authorization: `Bearer ${process.env.VAULT_GITHUB_USER_TOKEN}`, - "X-GitHub-Api-Version": this.gitHubApiVersion, - }, - }; - - if (body !== undefined) { - options.data = body; - } - - const resp = await context.fetch(url, options); - if (!suppressError && !resp.ok()) { + const response = await this.githubRequest(method, url, body); + if (!response.ok) { throw new Error( - `Failed to ${method} ${url}: ${resp.status()} ${resp.statusText()}`, - ); - } - - return resp; - } - - static async getGithubPaginatedRequest( - url: string, - pageNo = 1, - response: unknown[] = [], - ): Promise { - const fullUrl = url.includes("?") - ? `${url}&page=${pageNo}` - : `${url}?page=${pageNo}`; - const result = await GitHubApiHelper.githubRequest("GET", fullUrl); - const body = await result.json(); - - if (!Array.isArray(body)) { - throw new TypeError( - `Expected array but got ${typeof body}: ${JSON.stringify(body)}`, + `Failed to ${method} ${url}: ${response.status()} ${response.statusText()}`, ); } - if (body.length === 0) { - return response; - } - - return GitHubApiHelper.getGithubPaginatedRequest(url, pageNo + 1, [ - ...response, - ...body, - ]); - } - - /** - * Create a GitHub repository with a file - */ - public async createGitHubRepoWithFile( - owner: string, - repo: string, - filePath: string, - content: string, - ): Promise { - await GitHubApiHelper.githubRequest( - "POST", - `${this.apiUrl}/orgs/${owner}/repos`, - JSON.stringify({ - name: repo, - private: false, - // eslint-disable-next-line @typescript-eslint/naming-convention - auto_init: true, - }), - ); - - await new Promise((resolve) => setTimeout(resolve, 2000)); - - await GitHubApiHelper.githubRequest( - "PUT", - `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, - JSON.stringify({ - message: `Add ${filePath}`, - content: Buffer.from(content).toString("base64"), - }), - ); + return response; } /** * Update a file in a GitHub repository */ - public async updateFileInRepo( + static async updateFileInRepo( owner: string, repo: string, filePath: string, content: string, commitMessage: string, ): Promise { - const resp = await GitHubApiHelper.githubRequest( + const getFileResponse = await this.safeGithubRequest( "GET", - `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, + `${GITHUB_API_ENDPOINTS.contents(owner, repo)}/${filePath}`, ); - const fileData = (await resp.json()) as { sha: string }; - await GitHubApiHelper.githubRequest( + + const fileData = (await getFileResponse.json()) as { sha: string }; + + await this.safeGithubRequest( "PUT", - `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, + `${GITHUB_API_ENDPOINTS.contents(owner, repo)}/${filePath}`, JSON.stringify({ message: commitMessage, content: Buffer.from(content).toString("base64"), @@ -133,22 +56,21 @@ export class GitHubApiHelper { /** * Delete a file from a GitHub repository */ - public async deleteFileInRepo( + static async deleteFileInRepo( owner: string, repo: string, filePath: string, commitMessage: string, ): Promise { - const getFileResponse = await GitHubApiHelper.githubRequest( + const getFileResponse = await this.githubRequest( "GET", - `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, - undefined, - true, + `${GITHUB_API_ENDPOINTS.contents(owner, repo)}/${filePath}`, ); if (getFileResponse.status() === 404) { + console.log(`File ${filePath} already deleted or doesn't exist`); return; } - if (!getFileResponse.ok()) { + if (!getFileResponse.ok) { throw new Error( `Failed to get file: ${getFileResponse.status()} ${getFileResponse.statusText()}`, ); @@ -156,9 +78,9 @@ export class GitHubApiHelper { const fileData = (await getFileResponse.json()) as { sha: string }; - await GitHubApiHelper.githubRequest( + await this.safeGithubRequest( "DELETE", - `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, + `${GITHUB_API_ENDPOINTS.contents(owner, repo)}/${filePath}`, JSON.stringify({ message: commitMessage, sha: fileData.sha, @@ -166,31 +88,13 @@ export class GitHubApiHelper { ); } - /** - * Delete a GitHub repository - */ - public async deleteRepo(owner: string, repo: string): Promise { - const response = await GitHubApiHelper.githubRequest( - "DELETE", - `${this.apiUrl}/repos/${owner}/${repo}`, - undefined, - true, - ); - - if (!response.ok() && response.status() !== 404) { - throw new Error( - `Failed to delete repository: ${response.status()} ${response.statusText()}`, - ); - } - } - /** * Create a team in a GitHub organization */ - public async createTeamInOrg(org: string, teamName: string): Promise { - await GitHubApiHelper.githubRequest( + static async createTeamInOrg(org: string, teamName: string): Promise { + await this.safeGithubRequest( "POST", - `${this.apiUrl}/orgs/${org}/teams`, + `${getOrgUrl(org)}/teams`, JSON.stringify({ name: teamName, privacy: "closed", @@ -201,15 +105,13 @@ export class GitHubApiHelper { /** * Delete a team from a GitHub organization */ - public async deleteTeamFromOrg(org: string, teamName: string): Promise { - const response = await GitHubApiHelper.githubRequest( + static async deleteTeamFromOrg(org: string, teamName: string): Promise { + const response = await this.githubRequest( "DELETE", - `${this.apiUrl}/orgs/${org}/teams/${teamName}`, - undefined, - true, + `${getOrgUrl(org)}/teams/${teamName}`, ); - if (!response.ok() && response.status() !== 404) { + if (!response.ok && response.status() !== 404) { throw new Error( `Failed to delete team: ${response.status()} ${response.statusText()}`, ); @@ -219,14 +121,14 @@ export class GitHubApiHelper { /** * Add a user to a team in a GitHub organization */ - public async addUserToTeam( + static async addUserToTeam( org: string, teamName: string, username: string, ): Promise { - await GitHubApiHelper.githubRequest( + await this.safeGithubRequest( "PUT", - `${this.apiUrl}/orgs/${org}/teams/${teamName}/memberships/${username}`, + `${getOrgUrl(org)}/teams/${teamName}/memberships/${username}`, JSON.stringify({ role: "member", }), @@ -236,53 +138,44 @@ export class GitHubApiHelper { /** * Remove a user from a team in a GitHub organization */ - public async removeUserFromTeam( + static async removeUserFromTeam( org: string, teamName: string, username: string, ): Promise { - const response = await GitHubApiHelper.githubRequest( + const response = await this.githubRequest( "DELETE", - `${this.apiUrl}/orgs/${org}/teams/${teamName}/memberships/${username}`, - undefined, - true, + `${getOrgUrl(org)}/teams/${teamName}/memberships/${username}`, ); - if (!response.ok() && response.status() !== 404) { + if (!response.ok && response.status() !== 404) { throw new Error( `Failed to remove user from team: ${response.status()} ${response.statusText()}`, ); } } - public async getOrganizationReposUrl( + static async getOrganizationReposUrl( org = RHDH_GITHUB_TEST_ORGANIZATION, ): Promise { - const response = await GitHubApiHelper.githubRequest( - "GET", - `${this.apiUrl}/orgs/${org}`, - ); + const response = await this.safeGithubRequest("GET", getOrgUrl(org)); return (await response.json())["repos_url"]; } - public async getReposFromOrg(org = RHDH_GITHUB_TEST_ORGANIZATION) { + static async getReposFromOrg(org = RHDH_GITHUB_TEST_ORGANIZATION) { const reposUrl = await this.getOrganizationReposUrl(org); // GitHub defaults to 30; use 100 to reduce API calls. - return GitHubApiHelper.getGithubPaginatedRequest( - `${reposUrl}?per_page=100`, - ); + return this.getGithubPaginatedRequest(`${reposUrl}?per_page=100`); } - public async fileExistsInRepo( + static async fileExistsInRepo( owner: string, repo: string, filePath: string, ): Promise { - const resp = await GitHubApiHelper.githubRequest( + const resp = await this.githubRequest( "GET", - `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`, - undefined, - true, + `${GITHUB_API_ENDPOINTS.contents(owner, repo)}/${filePath}`, ); const status = resp.status(); return [200, 302, 304].includes(status); diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index f72632bd3..6549f0b68 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -5,7 +5,6 @@ import { RHDH_GITHUB_TEST_ORGANIZATION } from "../../support/constants/github/or test.describe("Github Discovery Catalog", () => { let catalogPage: CatalogPage; - let gitHubApiHelper: GitHubApiHelper; test.beforeAll(async ({ rhdh }) => { // Allow time for deployment + 1 min provider refresh delay + browser setup @@ -30,12 +29,11 @@ test.describe("Github Discovery Catalog", () => { test.beforeEach(async ({ loginHelper, page }) => { await loginHelper.loginAsGuest(); catalogPage = new CatalogPage(page); - gitHubApiHelper = new GitHubApiHelper(); await catalogPage.go(); }); test(`Discover Organization's Catalog`, async () => { - const organizationRepos = await gitHubApiHelper.getReposFromOrg( + const organizationRepos = await GitHubApiHelper.getReposFromOrg( RHDH_GITHUB_TEST_ORGANIZATION, ); const reposNames: string[] = (organizationRepos as Array<{ name?: string }>) @@ -48,7 +46,7 @@ test.describe("Github Discovery Catalog", () => { const reposWithCatalogInfo: string[] = ( await Promise.all( reposNames.map(async (repo) => - (await gitHubApiHelper.fileExistsInRepo( + (await GitHubApiHelper.fileExistsInRepo( RHDH_GITHUB_TEST_ORGANIZATION, repo, "catalog-info.yaml", diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts index 9972649ad..703b22c6f 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-events-module.spec.ts @@ -12,7 +12,6 @@ import { GitHubApiHelper } from "../../support/api/github-api-helper"; test.describe("GitHub Events Module", () => { let githubEventsHelper: GitHubEventsHelper; - let githubApiHelper: GitHubApiHelper; let staticToken: string; let rhdhBaseUrl: string; @@ -32,7 +31,6 @@ test.describe("GitHub Events Module", () => { rhdh.rhdhUrl, process.env.VAULT_GITHUB_APP_WEBHOOK_SECRET!, ); - githubApiHelper = new GitHubApiHelper(); rhdhBaseUrl = rhdh.rhdhUrl; }); @@ -101,7 +99,7 @@ spec: lifecycle: unknown owner: user:default/janus-qe`; - await githubApiHelper.createGitHubRepoWithFile( + await GitHubApiHelper.createGitHubRepoWithFile( catalogRepoDetails.owner, catalogRepoDetails.name, "catalog-info.yaml", @@ -146,7 +144,7 @@ spec: type: other lifecycle: unknown owner: user:default/janus-qe`; - await githubApiHelper.updateFileInRepo( + await GitHubApiHelper.updateFileInRepo( catalogRepoDetails.owner, catalogRepoDetails.name, "catalog-info.yaml", @@ -181,7 +179,7 @@ spec: }); test("Deleting an entity from the catalog", async ({ page, uiHelper }) => { - await githubApiHelper.deleteFileInRepo( + await GitHubApiHelper.deleteFileInRepo( catalogRepoDetails.owner, catalogRepoDetails.name, "catalog-info.yaml", @@ -213,7 +211,7 @@ spec: }); test.afterAll(async () => { - await githubApiHelper.deleteRepo( + await GitHubApiHelper.deleteGitHubRepo( catalogRepoDetails.owner, catalogRepoDetails.name, ); @@ -226,7 +224,7 @@ spec: const teamName = "test-team-" + Date.now(); test("Adding a new group", async ({ page, uiHelper }) => { - await githubApiHelper.createTeamInOrg("janus-qe", teamName); + await GitHubApiHelper.createTeamInOrg("janus-qe", teamName); await githubEventsHelper.sendTeamEvent("created", teamName, "janus-qe"); await expect @@ -250,7 +248,7 @@ spec: }); test("Deleting a group", async ({ page, uiHelper }) => { - await githubApiHelper.deleteTeamFromOrg("janus-qe", teamName); + await GitHubApiHelper.deleteTeamFromOrg("janus-qe", teamName); await githubEventsHelper.sendTeamEvent("deleted", teamName, "janus-qe"); @@ -326,7 +324,7 @@ spec: teamName = "test-team-" + Date.now(); - await githubApiHelper.createTeamInOrg("janus-qe", teamName); + await GitHubApiHelper.createTeamInOrg("janus-qe", teamName); teamCreated = true; await githubEventsHelper.sendTeamEvent("created", teamName, "janus-qe"); @@ -336,7 +334,7 @@ spec: test.afterEach(async () => { if (userAddedToTeam) { - await githubApiHelper.removeUserFromTeam( + await GitHubApiHelper.removeUserFromTeam( "janus-qe", teamName, "rhdh-qe", @@ -345,13 +343,13 @@ spec: } if (teamCreated) { - await githubApiHelper.deleteTeamFromOrg("janus-qe", teamName); + await GitHubApiHelper.deleteTeamFromOrg("janus-qe", teamName); teamCreated = false; } }); test("Adding a user to a group", async ({ uiHelper }) => { - await githubApiHelper.addUserToTeam("janus-qe", teamName, "rhdh-qe"); + await GitHubApiHelper.addUserToTeam("janus-qe", teamName, "rhdh-qe"); userAddedToTeam = true; await githubEventsHelper.sendMembershipEvent( @@ -381,7 +379,7 @@ spec: }); test("Removing a user from a group", async ({ uiHelper }) => { - await githubApiHelper.addUserToTeam("janus-qe", teamName, "rhdh-qe"); + await GitHubApiHelper.addUserToTeam("janus-qe", teamName, "rhdh-qe"); userAddedToTeam = true; await githubEventsHelper.sendMembershipEvent( @@ -407,7 +405,7 @@ spec: ) .toContain("rhdh-qe"); - await githubApiHelper.removeUserFromTeam( + await GitHubApiHelper.removeUserFromTeam( "janus-qe", teamName, "rhdh-qe", From eb9fcdad453bb2550a190dc65b78c0a808fedfe8 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Wed, 8 Apr 2026 17:07:42 +0200 Subject: [PATCH 23/25] Update e2e-test-utils version Signed-off-by: Dominika Zemanovicova --- workspaces/backstage/e2e-tests/package.json | 2 +- .../support/api/github-api-helper.ts | 16 +++++++-------- workspaces/backstage/e2e-tests/yarn.lock | 20 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/workspaces/backstage/e2e-tests/package.json b/workspaces/backstage/e2e-tests/package.json index 17cbd9628..4227f1f54 100644 --- a/workspaces/backstage/e2e-tests/package.json +++ b/workspaces/backstage/e2e-tests/package.json @@ -24,7 +24,7 @@ "devDependencies": { "@eslint/js": "^9.39.2", "@playwright/test": "1.57.0", - "@red-hat-developer-hub/e2e-test-utils": "1.1.25", + "@red-hat-developer-hub/e2e-test-utils": "1.1.28", "@types/node": "^24.10.1", "dotenv": "^16.4.7", "eslint": "^9.39.2", diff --git a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts index a4fed70df..5f348c0fb 100644 --- a/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts +++ b/workspaces/backstage/e2e-tests/support/api/github-api-helper.ts @@ -5,9 +5,6 @@ import { GITHUB_API_ENDPOINTS, } from "@red-hat-developer-hub/e2e-test-utils/helpers"; -const baseApiUrl = "https://api.github.com"; -const getOrgUrl = (owner: string) => `${baseApiUrl}/orgs/${owner}`; - // https://docs.github.com/en/rest?apiVersion=2022-11-28 export class GitHubApiHelper extends APIHelper { static async safeGithubRequest( @@ -94,7 +91,7 @@ export class GitHubApiHelper extends APIHelper { static async createTeamInOrg(org: string, teamName: string): Promise { await this.safeGithubRequest( "POST", - `${getOrgUrl(org)}/teams`, + `${GITHUB_API_ENDPOINTS.getOrg(org)}/teams`, JSON.stringify({ name: teamName, privacy: "closed", @@ -108,7 +105,7 @@ export class GitHubApiHelper extends APIHelper { static async deleteTeamFromOrg(org: string, teamName: string): Promise { const response = await this.githubRequest( "DELETE", - `${getOrgUrl(org)}/teams/${teamName}`, + `${GITHUB_API_ENDPOINTS.getOrg(org)}/teams/${teamName}`, ); if (!response.ok && response.status() !== 404) { @@ -128,7 +125,7 @@ export class GitHubApiHelper extends APIHelper { ): Promise { await this.safeGithubRequest( "PUT", - `${getOrgUrl(org)}/teams/${teamName}/memberships/${username}`, + `${GITHUB_API_ENDPOINTS.getOrg(org)}/teams/${teamName}/memberships/${username}`, JSON.stringify({ role: "member", }), @@ -145,7 +142,7 @@ export class GitHubApiHelper extends APIHelper { ): Promise { const response = await this.githubRequest( "DELETE", - `${getOrgUrl(org)}/teams/${teamName}/memberships/${username}`, + `${GITHUB_API_ENDPOINTS.getOrg(org)}/teams/${teamName}/memberships/${username}`, ); if (!response.ok && response.status() !== 404) { @@ -158,7 +155,10 @@ export class GitHubApiHelper extends APIHelper { static async getOrganizationReposUrl( org = RHDH_GITHUB_TEST_ORGANIZATION, ): Promise { - const response = await this.safeGithubRequest("GET", getOrgUrl(org)); + const response = await this.safeGithubRequest( + "GET", + GITHUB_API_ENDPOINTS.getOrg(org), + ); return (await response.json())["repos_url"]; } diff --git a/workspaces/backstage/e2e-tests/yarn.lock b/workspaces/backstage/e2e-tests/yarn.lock index eacec8073..6b43c113a 100644 --- a/workspaces/backstage/e2e-tests/yarn.lock +++ b/workspaces/backstage/e2e-tests/yarn.lock @@ -188,13 +188,13 @@ __metadata: languageName: node linkType: hard -"@keycloak/keycloak-admin-client@npm:^26.0.0": - version: 26.5.2 - resolution: "@keycloak/keycloak-admin-client@npm:26.5.2" +"@keycloak/keycloak-admin-client@npm:26.5.6": + version: 26.5.6 + resolution: "@keycloak/keycloak-admin-client@npm:26.5.6" dependencies: camelize-ts: ^3.0.0 url-template: ^3.1.1 - checksum: 3ceedb4347880c87d93dd3644e062602139959f649ee79e6eeb570814c8c0b640ebff69c682855deba6e311a6f9c7486a88b9ff2cf29d80e9f68539771bfd120 + checksum: 1e996590de8f191dbe616622102895b358c4e31274a7770b3bba2039fc2e64ce9a67ca6cbf6e823d39e061891c25c29b4342b199f20cc3e04937509cf3975843 languageName: node linkType: hard @@ -303,14 +303,14 @@ __metadata: languageName: node linkType: hard -"@red-hat-developer-hub/e2e-test-utils@npm:1.1.25": - version: 1.1.25 - resolution: "@red-hat-developer-hub/e2e-test-utils@npm:1.1.25" +"@red-hat-developer-hub/e2e-test-utils@npm:1.1.28": + version: 1.1.28 + resolution: "@red-hat-developer-hub/e2e-test-utils@npm:1.1.28" dependencies: "@axe-core/playwright": ^4.11.0 "@backstage-community/plugin-rbac-common": 1.23.0 "@eslint/js": ^9.39.1 - "@keycloak/keycloak-admin-client": ^26.0.0 + "@keycloak/keycloak-admin-client": 26.5.6 "@kubernetes/client-node": ^1.4.0 eslint: ^9.39.1 eslint-plugin-check-file: ^3.3.1 @@ -327,7 +327,7 @@ __metadata: zx: ^8.8.5 peerDependencies: "@playwright/test": ^1.57.0 - checksum: 93af19ccab0bff70e907a23b6e4341ed14c27df1863a72c8c5248b11e067542bfee955a56acd300df3f4d32db9ce2a674803dcf03025f43c7aeb09bd6562229b + checksum: f0787bbbeb76942ef2289d1f0e0e7929f892ecda7d06485f12b72e6623f6a7440b0a2309c7feb6423eeb589d9c04e6e1ebba88659db0d6507f7d61f7131c4a58 languageName: node linkType: hard @@ -630,7 +630,7 @@ __metadata: dependencies: "@eslint/js": ^9.39.2 "@playwright/test": 1.57.0 - "@red-hat-developer-hub/e2e-test-utils": 1.1.25 + "@red-hat-developer-hub/e2e-test-utils": 1.1.28 "@types/node": ^24.10.1 dotenv: ^16.4.7 eslint: ^9.39.2 From b61481951a6d018e3f2008f7567ae2969d635942 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Tue, 14 Apr 2026 08:56:49 +0200 Subject: [PATCH 24/25] Remove test.info Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/tests/specs/github-discovery.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts index 6549f0b68..ce7eae3e2 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/github-discovery.spec.ts @@ -10,11 +10,6 @@ test.describe("Github Discovery Catalog", () => { // Allow time for deployment + 1 min provider refresh delay + browser setup test.setTimeout(10 * 60 * 1000); - test.info().annotations.push({ - type: "component", - description: "api", - }); - await rhdh.configure({ auth: "guest", appConfig: "tests/config/github-discovery/app-config-rhdh.yaml", From 6521615f2ace56148a4f9637df261bc5aedea0b7 Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Thu, 23 Apr 2026 09:52:53 +0200 Subject: [PATCH 25/25] Update removed func after merge Signed-off-by: Dominika Zemanovicova --- .../backstage/e2e-tests/tests/specs/gitlab-discovery.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/backstage/e2e-tests/tests/specs/gitlab-discovery.spec.ts b/workspaces/backstage/e2e-tests/tests/specs/gitlab-discovery.spec.ts index 8a21a0d6c..c3e06b87c 100644 --- a/workspaces/backstage/e2e-tests/tests/specs/gitlab-discovery.spec.ts +++ b/workspaces/backstage/e2e-tests/tests/specs/gitlab-discovery.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@red-hat-developer-hub/e2e-test-utils/test"; -import { requireEnv } from "../../support/utils/require-env"; +import { requireEnv } from "@red-hat-developer-hub/e2e-test-utils/utils"; test.describe("gitlab discovery UI tests", () => { test.beforeAll(async ({ rhdh }) => {