Skip to content

Commit 190ecd6

Browse files
authored
[ZEPPELIN-6336] Enable Conditional Login Test Based on shiro.ini Presence in Zeppelin
### What is this PR for? Currently, Zeppelin(zeppelin-web-angular)’s E2E authentication tests require the presence of a `shiro.ini` file to run. However, in certain build or CI environments, this file may not exist. In such cases, login tests may fail or behave unpredictably. To improve flexibility, the test framework should support both scenarios: - **Auth mode (`shiro.ini` exists)** → Run all tests, including authentication/login tests - **Anonymous mode (`shiro.ini` does not exist)** → Skip authentication/login tests, but run all other tests #### 1. GitHub Actions Workflow (Matrix Mode) - Added `strategy.matrix.mode: [anonymous, auth]` - In `auth` mode, copy `shiro.ini.template → shiro.ini` - In `anonymous` mode, skip `shiro.ini` setup to simulate a no-auth environment #### 2. Playwright Global Setup / Teardown - **`global-setup.ts`** - Added `LoginTestUtil.isShiroEnabled()` to detect presence of `shiro.ini` - If enabled → load credentials & run login tests - If disabled → skip login tests, log message - **`global-teardown.ts`** - Added environment cleanup (e.g., reset cache) #### 3. Authentication Utility (`login-page.util.ts`) - `isShiroEnabled()`: Checks if `shiro.ini` is accessible via `fs.access` - `getTestCredentials()`: Parses credentials only when `shiro.ini` exists - `resetCache()`: Clears cached values between test runs #### 4. Test Code Updates - **`app.spec.ts`** - Conditionally checks whether login page or workspace should be visible, based on `isShiroEnabled()` - **Other Playwright tests** - Authentication-related tests are skipped when `shiro.ini` is not present ### What type of PR is it? Improvement ### Todos ### What is the Jira issue? ZEPPELIN-6336 ### How should this be tested? ### Screenshots (if appropriate) ### Questions: * Does the license files need to update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Closes #5088 from dididy/fix/ZEPPELIN-6336. Signed-off-by: ChanHo Lee <[email protected]>
1 parent 7c7b00f commit 190ecd6

File tree

8 files changed

+375
-3
lines changed

8 files changed

+375
-3
lines changed

.github/workflows/frontend.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ jobs:
6262

6363
run-playwright-e2e-tests:
6464
runs-on: ubuntu-24.04
65+
strategy:
66+
matrix:
67+
mode: [anonymous, auth]
6568
steps:
6669
- name: Checkout
6770
uses: actions/checkout@v4
@@ -85,13 +88,19 @@ jobs:
8588
${{ runner.os }}-zeppelin-
8689
- name: Install application
8790
run: ./mvnw clean install -DskipTests -am -pl zeppelin-web-angular ${MAVEN_ARGS}
91+
- name: Setup Zeppelin Server (Shiro.ini)
92+
run: |
93+
export ZEPPELIN_CONF_DIR=./conf
94+
if [ "${{ matrix.mode }}" != "anonymous" ]; then
95+
cp conf/shiro.ini.template conf/shiro.ini
96+
fi
8897
- name: Run headless E2E test with Maven
8998
run: xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" ./mvnw verify -pl zeppelin-web-angular -Pweb-e2e ${MAVEN_ARGS}
90-
- name: Upload Playwright report
99+
- name: Upload Playwright Report
91100
uses: actions/upload-artifact@v4
92101
if: always()
93102
with:
94-
name: playwright-report
103+
name: playwright-report-${{ matrix.mode }}
95104
path: zeppelin-web-angular/playwright-report/
96105
retention-days: 30
97106
- name: Print Zeppelin logs
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License.
11+
*/
12+
13+
import { LoginTestUtil } from './models/login-page.util';
14+
15+
async function globalSetup() {
16+
console.log('🔧 Global Setup: Checking Shiro configuration...');
17+
18+
// Reset cache to ensure fresh check
19+
LoginTestUtil.resetCache();
20+
21+
const isShiroEnabled = await LoginTestUtil.isShiroEnabled();
22+
23+
if (isShiroEnabled) {
24+
console.log('✅ Shiro.ini detected - authentication tests will run');
25+
26+
// Parse and validate credentials
27+
const credentials = await LoginTestUtil.getTestCredentials();
28+
const userCount = Object.keys(credentials).length;
29+
30+
console.log(`📋 Found ${userCount} test credentials in shiro.ini`);
31+
} else {
32+
console.log('⚠️ Shiro.ini not found - authentication tests will be skipped');
33+
}
34+
}
35+
36+
export default globalSetup;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License.
11+
*/
12+
13+
import { LoginTestUtil } from './models/login-page.util';
14+
15+
async function globalTeardown() {
16+
console.log('🧹 Global Teardown: Cleaning up test environment...');
17+
18+
LoginTestUtil.resetCache();
19+
console.log('✅ Test cache cleared');
20+
}
21+
22+
export default globalTeardown;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License.
11+
*/
12+
13+
import { Locator, Page } from '@playwright/test';
14+
import { BasePage } from './base-page';
15+
16+
export class LoginPage extends BasePage {
17+
readonly userNameInput: Locator;
18+
readonly passwordInput: Locator;
19+
readonly loginButton: Locator;
20+
readonly welcomeTitle: Locator;
21+
readonly formContainer: Locator;
22+
23+
constructor(page: Page) {
24+
super(page);
25+
this.userNameInput = page.getByRole('textbox', { name: 'User Name' });
26+
this.passwordInput = page.getByRole('textbox', { name: 'Password' });
27+
this.loginButton = page.getByRole('button', { name: 'Login' });
28+
this.welcomeTitle = page.getByRole('heading', { name: 'Welcome to Zeppelin!' });
29+
this.formContainer = page.locator('form[nz-form]');
30+
}
31+
32+
async navigate(): Promise<void> {
33+
await this.page.goto('/#/login');
34+
await this.waitForPageLoad();
35+
}
36+
37+
async login(username: string, password: string): Promise<void> {
38+
await this.userNameInput.fill(username);
39+
await this.passwordInput.fill(password);
40+
await this.loginButton.click();
41+
}
42+
43+
async waitForErrorMessage(): Promise<void> {
44+
await this.page.waitForSelector("text=The username and password that you entered don't match.", { timeout: 5000 });
45+
}
46+
47+
async getErrorMessageText(): Promise<string> {
48+
return (
49+
(await this.page
50+
.locator("text=The username and password that you entered don't match.")
51+
.first()
52+
.textContent()) || ''
53+
);
54+
}
55+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License.
11+
*/
12+
13+
import * as fs from 'fs';
14+
import * as path from 'path';
15+
import { promisify } from 'util';
16+
17+
const access = promisify(fs.access);
18+
const readFile = promisify(fs.readFile);
19+
20+
export interface TestCredentials {
21+
username: string;
22+
password: string;
23+
roles?: string[];
24+
}
25+
26+
export class LoginTestUtil {
27+
private static readonly SHIRO_CONFIG_PATH = path.join(process.cwd(), '..', 'conf', 'shiro.ini');
28+
29+
private static _testCredentials: Record<string, TestCredentials> | null = null;
30+
private static _isShiroEnabled: boolean | null = null;
31+
32+
static resetCache(): void {
33+
this._testCredentials = null;
34+
this._isShiroEnabled = null;
35+
}
36+
37+
static async isShiroEnabled(): Promise<boolean> {
38+
if (this._isShiroEnabled !== null) {
39+
return this._isShiroEnabled;
40+
}
41+
42+
try {
43+
await access(this.SHIRO_CONFIG_PATH);
44+
this._isShiroEnabled = true;
45+
} catch (error) {
46+
this._isShiroEnabled = false;
47+
}
48+
49+
return this._isShiroEnabled;
50+
}
51+
52+
static async getTestCredentials(): Promise<Record<string, TestCredentials>> {
53+
if (!(await this.isShiroEnabled())) {
54+
return {};
55+
}
56+
57+
if (this._testCredentials !== null) {
58+
return this._testCredentials;
59+
}
60+
61+
try {
62+
const content = await readFile(this.SHIRO_CONFIG_PATH, 'utf-8');
63+
const users: Record<string, TestCredentials> = {};
64+
65+
this._parseUsersSection(content, users);
66+
this._addTestCredentials(users);
67+
68+
this._testCredentials = users;
69+
return users;
70+
} catch (error) {
71+
console.error('Failed to parse shiro.ini:', error);
72+
return {};
73+
}
74+
}
75+
76+
private static _parseUsersSection(content: string, users: Record<string, TestCredentials>): void {
77+
const lines = content.split('\n');
78+
let inUsersSection = false;
79+
80+
for (const line of lines) {
81+
const trimmedLine = line.trim();
82+
83+
if (trimmedLine === '[users]') {
84+
inUsersSection = true;
85+
continue;
86+
}
87+
88+
if (trimmedLine.startsWith('[') && trimmedLine !== '[users]') {
89+
inUsersSection = false;
90+
continue;
91+
}
92+
93+
if (inUsersSection && trimmedLine && !trimmedLine.startsWith('#')) {
94+
this._parseUserLine(trimmedLine, users);
95+
}
96+
}
97+
}
98+
99+
private static _parseUserLine(line: string, users: Record<string, TestCredentials>): void {
100+
const [userPart, ...roleParts] = line.split('=');
101+
if (!userPart || roleParts.length === 0) return;
102+
103+
const username = userPart.trim();
104+
const rightSide = roleParts.join('=').trim();
105+
const parts = rightSide.split(',').map(p => p.trim());
106+
107+
if (parts.length > 0) {
108+
const password = parts[0];
109+
const roles = parts.slice(1);
110+
111+
users[username] = {
112+
username,
113+
password,
114+
roles
115+
};
116+
}
117+
}
118+
119+
private static _addTestCredentials(users: Record<string, TestCredentials>): void {
120+
users.INVALID_USER = { username: 'wronguser', password: 'wrongpass' };
121+
users.EMPTY_CREDENTIALS = { username: '', password: '' };
122+
}
123+
}

zeppelin-web-angular/e2e/tests/app.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import { expect, test } from '@playwright/test';
1414
import { ZeppelinHelper } from '../helper';
1515
import { BasePage } from '../models/base-page';
16+
import { LoginTestUtil } from '../models/login-page.util';
1617
import { addPageAnnotationBeforeEach, PAGES } from '../utils';
1718

1819
test.describe('Zeppelin App Component', () => {
@@ -56,7 +57,12 @@ test.describe('Zeppelin App Component', () => {
5657

5758
test('should display workspace after loading', async ({ page }) => {
5859
await zeppelinHelper.waitForZeppelinReady();
59-
await expect(page.locator('zeppelin-workspace')).toBeVisible();
60+
const isShiroEnabled = await LoginTestUtil.isShiroEnabled();
61+
if (isShiroEnabled) {
62+
await expect(page.locator('zeppelin-login')).toBeVisible();
63+
} else {
64+
await expect(page.locator('zeppelin-workspace')).toBeVisible();
65+
}
6066
});
6167

6268
test('should handle navigation events correctly', async ({ page }) => {

0 commit comments

Comments
 (0)