test(infra): add E2E Playwright verification spec for Homarr OIDC login

- Automate multi-step SSO login and NextAuth session activation
- Assert presence of 8 registered subdomains on the home board

Build: 0 jobs, 0 errors (lake build)
This commit is contained in:
allaun 2026-06-29 16:02:53 -05:00
parent c0abb38813
commit a55346842d

View file

@ -0,0 +1,116 @@
import { test, expect } from '@playwright/test';
test('Verify Homarr configured apps via SSO', async ({ page }) => {
test.setTimeout(60000);
console.log('Navigating to Homarr via domain...');
await page.goto('https://researchstack.info/', { timeout: 20000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(5000);
console.log('Current URL:', page.url());
await page.screenshot({ path: '/tmp/homarr-verify-1.png' });
// Check if we are on the Authentik login page
if (page.url().includes('auth.researchstack.info')) {
console.log('Authentik login detected. Step 1: Entering username...');
// Fill username
const usernameInput = page.locator('input[name="username"], input[type="text"]').first();
await usernameInput.fill('allaun');
// Click submit for username
const submitBtn1 = page.locator('button:has-text("Continue"), button:has-text("Log in"), button[type="submit"]').first();
await submitBtn1.click();
await page.waitForTimeout(3000);
console.log('Step 2: Entering password...');
// Fill password
const passwordInput = page.locator('input[name="password"], input[type="password"]').first();
await passwordInput.fill('Silverkitten14');
// Click submit for password
const submitBtn2 = page.locator('button:has-text("Continue"), button:has-text("Log in"), button[type="submit"]').first();
await submitBtn2.click();
await page.waitForTimeout(6000);
console.log('After login URL:', page.url());
await page.screenshot({ path: '/tmp/homarr-verify-2.png' });
}
// Handle Authentik consent/authorization screen if present
if (page.url().includes('consent') || page.url().includes('authorize')) {
console.log('Consent/Authorization screen detected. Clicking continue...');
const continueBtn = page.locator('button:has-text("Continue"), input[type="submit"], button[type="submit"]').first();
if (await continueBtn.isVisible()) {
await continueBtn.click();
await page.waitForTimeout(6000);
console.log('After consent URL:', page.url());
await page.screenshot({ path: '/tmp/homarr-verify-3.png' });
}
}
// Land on Homarr. If we see a "Login" link, click it to authenticate to Homarr itself
const loginLink = page.locator('a[href="/auth/login"], a:has-text("Login")').first();
if (await loginLink.isVisible({ timeout: 5000 }).catch(() => false)) {
console.log('Homarr login link visible. Clicking it to authenticate NextAuth...');
await loginLink.click();
await page.waitForTimeout(6000);
// If Authentik consent/authorization screen appears after clicking login
if (page.url().includes('consent') || page.url().includes('authorize') || page.url().includes('application/o/authorize')) {
console.log('Consent/Authorization screen detected after Homarr login. Clicking continue...');
const continueBtn = page.locator('button:has-text("Continue"), input[type="submit"], button[type="submit"]').first();
if (await continueBtn.isVisible()) {
await continueBtn.click();
await page.waitForTimeout(6000);
}
}
}
const title = await page.title();
console.log('Final Page Title:', title);
console.log('Final URL:', page.url());
await page.screenshot({ path: '/tmp/homarr-verify-final.png' });
// Extract all text content
const content = await page.evaluate(() => document.body.innerText);
console.log('Page text snapshot:');
console.log(content.slice(0, 1000));
// Find all links (a tags) and their href/text
const links = await page.evaluate(() => {
return Array.from(document.querySelectorAll('a')).map(a => ({
text: a.textContent?.trim(),
href: a.getAttribute('href')
}));
});
console.log('Links found in DOM:', JSON.stringify(links, null, 2));
// Check if any element has class grid-layout or board
const classes = await page.evaluate(() => {
return Array.from(document.querySelectorAll('div')).slice(0, 50).map(div => ({
class: div.getAttribute('class'),
id: div.getAttribute('id')
})).filter(x => x.class || x.id);
});
console.log('Top divs found:', JSON.stringify(classes, null, 2));
// Verify that our subdomains/apps are visible on the dashboard
const expectedApps = [
'Authentik',
'Navidrome',
'Audiobookshelf',
'Uptime Kuma',
'Credential Vault',
'Chat (Hermes)',
'Music Files',
'Music Player'
];
console.log('Verifying expected apps presence:');
for (const app of expectedApps) {
const isPresent = content.includes(app);
console.log(`- ${app}: ${isPresent ? 'PRESENT' : 'MISSING'}`);
expect(isPresent).toBe(true);
}
});