Skip to content

Commit d5f7601

Browse files
Copilothairmare
andcommitted
feat: restore OIDC sign-in, guest only in dev/CI, extend Playwright tests
Co-authored-by: hairmare <116588+hairmare@users.noreply.github.com> Agent-Logs-Url: https://github.com/radiorabe/rabe-backstage/sessions/6026c0b9-1370-4843-a064-7cea57c81c0f
1 parent e256940 commit d5f7601

12 files changed

Lines changed: 199 additions & 14 deletions

File tree

app-config.yaml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,20 @@ techdocs:
6767

6868
auth:
6969
# see https://backstage.io/docs/auth/ to learn about auth providers
70-
environment: production
70+
environment: development
7171
### Providing an auth.session.secret will enable session support in the auth-backend
7272
session:
7373
secret: ${SESSION_SECRET}
7474
providers:
7575
oidc:
76+
development:
77+
prompt: auto
78+
metadataUrl: ${AUTH_KEYCLOAK_METADATA_URL}
79+
clientId: ${AUTH_KEYCLOAK_CLIENT_ID}
80+
clientSecret: ${AUTH_KEYCLOAK_CLIENT_SECRET}
81+
signIn:
82+
resolvers:
83+
- resolver: emailMatchingUserEntityProfileEmail
7684
production:
7785
prompt: auto
7886
metadataUrl: ${AUTH_KEYCLOAK_METADATA_URL}
@@ -82,6 +90,9 @@ auth:
8290
resolvers:
8391
- resolver: emailMatchingUserEntityProfileEmail
8492
github:
93+
development:
94+
clientId: ${AUTH_GITHUB_CLIENT_ID}
95+
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
8596
production:
8697
clientId: ${AUTH_GITHUB_CLIENT_ID}
8798
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}

packages/app/e2e-tests/app.test.ts

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,84 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { test, expect } from '@playwright/test';
16+
import { test, expect, type Page } from '@playwright/test';
17+
18+
// Extra time for Backstage to poll popup.closed and update React state.
19+
const AUTH_ERROR_TIMEOUT_MS = 15_000;
20+
// Tolerance for the ~214-pixel layout shift caused by the error text
21+
// inserting into the card and nudging the Sign In button by ~1 px.
22+
const AUTH_ERROR_MAX_DIFF_PIXELS = 300;
23+
24+
/** Sign in as a Guest (accepts the backend-unavailable fallback dialog). */
25+
async function signInAsGuest(page: Page) {
26+
page.on('dialog', dialog => dialog.accept());
27+
await page.goto('/');
28+
await page.getByRole('button', { name: 'Enter' }).click();
29+
}
1730

1831
test('App should render the sign-in screen', async ({ page }) => {
1932
await page.goto('/');
2033

2134
await expect(page).toHaveTitle(/RaBe Backstage/);
2235

23-
const enterButton = page.getByRole('button', { name: 'Enter' });
24-
await expect(enterButton).toBeVisible();
36+
// OIDC sign-in button is always visible.
37+
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible();
38+
// Guest sign-in button is visible in dev / CI (auth.environment = development).
39+
await expect(page.getByRole('button', { name: 'Enter' })).toBeVisible();
2540

2641
await expect(page).toHaveScreenshot('login-page.png');
2742
});
2843

29-
test('Guest sign-in should navigate to the main application', async ({ page }) => {
30-
// Accept any browser confirm dialogs (e.g. fallback to legacy guest token when the auth backend is unavailable)
31-
page.on('dialog', dialog => dialog.accept());
44+
test('Sign-in should show an auth error when no IdP is available', async ({
45+
page,
46+
context,
47+
}) => {
48+
// Register handler before navigating so auto sign-in popups are also closed.
49+
context.on('page', async popup => {
50+
await popup.close();
51+
});
3252

3353
await page.goto('/');
3454

35-
const enterButton = page.getByRole('button', { name: 'Enter' });
36-
await expect(enterButton).toBeVisible();
55+
const signInButton = page.getByRole('button', { name: 'Sign In' });
56+
await expect(signInButton).toBeVisible();
57+
58+
// Wait for the initial auto sign-in attempt to settle before clicking.
59+
await page.waitForLoadState('networkidle');
60+
61+
await signInButton.click();
62+
63+
// Backstage polls popup.closed on an interval, so allow extra time.
64+
await expect(page.getByText('Login failed, popup was closed')).toBeVisible({
65+
timeout: AUTH_ERROR_TIMEOUT_MS,
66+
});
67+
68+
// Wait for the layout to settle after the error text causes a reflow.
69+
await expect(signInButton).toBeVisible();
70+
71+
await expect(page).toHaveScreenshot('login-auth-error.png', {
72+
maxDiffPixels: AUTH_ERROR_MAX_DIFF_PIXELS,
73+
});
74+
});
75+
76+
test('Guest sign-in should load the catalog page', async ({ page }) => {
77+
await signInAsGuest(page);
78+
79+
await expect(
80+
page.getByRole('heading', { name: 'Radio Bern RaBe Catalog' }),
81+
).toBeVisible();
82+
83+
await expect(page).toHaveScreenshot('catalog-page.png');
84+
});
85+
86+
test('Settings page should be accessible after sign-in', async ({ page }) => {
87+
await signInAsGuest(page);
88+
89+
await page.getByRole('link', { name: 'Settings' }).click();
3790

38-
await enterButton.click();
91+
await expect(
92+
page.getByRole('heading', { name: 'Settings' }),
93+
).toBeVisible();
3994

40-
// After sign-in, the catalog heading should be visible
41-
await expect(page.getByRole('heading', { name: 'Radio Bern RaBe Catalog' })).toBeVisible();
95+
await expect(page).toHaveScreenshot('settings-page.png');
4296
});
52.7 KB
Loading
58.3 KB
Loading
5.21 KB
Loading
60 KB
Loading

packages/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@backstage-community/plugin-github-pull-requests-board": "^0.15.1",
2020
"@backstage-community/plugin-todo": "^0.17.1",
2121
"@backstage/cli": "backstage:^",
22+
"@backstage/core-app-api": "backstage:^",
2223
"@backstage/core-components": "backstage:^",
2324
"@backstage/core-plugin-api": "backstage:^",
2425
"@backstage/frontend-defaults": "backstage:^",

packages/app/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { createApp } from '@backstage/frontend-defaults';
22
import catalogPlugin from '@backstage/plugin-catalog/alpha';
33
import { navModule } from './modules/nav';
4+
import { authModule } from './modules/auth';
45

56
export default createApp({
6-
features: [catalogPlugin, navModule],
7+
features: [catalogPlugin, navModule, authModule],
78
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright Radio Bern RaBe
3+
*
4+
* SPDX-FileCopyrightText: 2024 Radio Bern RaBe <https://rabe.ch>
5+
* SPDX-License-Identifier: AGPL-3.0-only
6+
*/
7+
8+
import {
9+
createFrontendModule,
10+
ApiBlueprint,
11+
configApiRef,
12+
discoveryApiRef,
13+
oauthRequestApiRef,
14+
useApi,
15+
} from '@backstage/frontend-plugin-api';
16+
import { SignInPageBlueprint } from '@backstage/plugin-app-react';
17+
import { SignInPage } from '@backstage/core-components';
18+
import { OAuth2 } from '@backstage/core-app-api';
19+
import { oidcAuthApiRef } from './oidcAuthApiRef';
20+
import type { SignInPageProps } from '@backstage/core-plugin-api';
21+
22+
type SignInProvider =
23+
| 'guest'
24+
| { id: string; title: string; message: string; apiRef: typeof oidcAuthApiRef };
25+
26+
const oidcApiExtension = ApiBlueprint.make({
27+
name: 'oidc',
28+
params: defineParams =>
29+
defineParams({
30+
api: oidcAuthApiRef,
31+
deps: {
32+
discoveryApi: discoveryApiRef,
33+
oauthRequestApi: oauthRequestApiRef,
34+
configApi: configApiRef,
35+
},
36+
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
37+
OAuth2.create({
38+
discoveryApi,
39+
oauthRequestApi,
40+
configApi,
41+
provider: {
42+
id: 'oidc',
43+
title: 'RaBe SSO',
44+
icon: () => null,
45+
},
46+
defaultScopes: ['openid', 'profile', 'email', 'offline_access'],
47+
}),
48+
}),
49+
});
50+
51+
const rabeSignInPage = SignInPageBlueprint.make({
52+
params: {
53+
loader: async () => {
54+
const RabeSignInPage = (props: SignInPageProps) => {
55+
const configApi = useApi(configApiRef);
56+
// Show Guest only in non-production environments (dev, CI).
57+
// auth.environment is set to 'development' in app-config.yaml and
58+
// overridden to 'production' in app-config.production.yaml.
59+
const isDev =
60+
configApi.getOptionalString('auth.environment') !== 'production';
61+
const providers: SignInProvider[] = [
62+
{
63+
id: 'oidc-auth-provider',
64+
title: 'RaBe SSO',
65+
message: 'Sign in with your RaBe Keycloak account',
66+
apiRef: oidcAuthApiRef,
67+
},
68+
...(isDev ? (['guest'] as const) : []),
69+
];
70+
return <SignInPage {...props} providers={providers} />;
71+
};
72+
return RabeSignInPage;
73+
},
74+
},
75+
});
76+
77+
export const authModule = createFrontendModule({
78+
pluginId: 'app',
79+
extensions: [oidcApiExtension, rabeSignInPage],
80+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright Radio Bern RaBe
3+
*
4+
* SPDX-FileCopyrightText: 2024 Radio Bern RaBe <https://rabe.ch>
5+
* SPDX-License-Identifier: AGPL-3.0-only
6+
*/
7+
8+
export { authModule } from './authModule';
9+
export { oidcAuthApiRef } from './oidcAuthApiRef';

0 commit comments

Comments
 (0)