mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
801a098a5b
* feat(files): backend foundation for stack file explorer
Install multer for multipart file upload handling. Add
isValidRelativeStackPath to validation.ts to guard client-supplied
relative paths against traversal, absolute paths, NUL bytes, backslash
injection, and double-slash segments. Add isBinaryBuffer to a new
binaryDetect.ts utility for heuristic text/binary detection via
NUL-byte fast exit and non-printable byte ratio sampling.
* fix(files): reject bare dot segments in isValidRelativeStackPath
* feat(files): add safe stack-scoped file I/O methods to FileSystemService
Adds FileEntry interface and seven new public methods to FileSystemService
for stack-scoped file operations: listStackDirectory, readStackFile,
streamStackFile, writeStackFile, deleteStackPath, mkdirStackPath, and
statStackEntry.
Each method routes through a private resolveSafeStackPath helper that
enforces two-phase path containment: a pre-realpath lexical check plus a
post-realpath symlink-escape check. ENOENT targets are handled by walking
up to the deepest existing ancestor, realpaths that ancestor, and
reattaching the remaining suffix.
Binary detection delegates to isBinaryBuffer; path safety delegates to
isPathWithinBase. Protected file names and the MIME map are module-level
constants to avoid repeated allocation.
* feat(files): frontend API wrappers and Monaco language helper
* fix(files): tighten stackFilesApi error handling and localOnly support
* fix(files): FileSystemService safety and correctness fixes
* feat(files): add file explorer API endpoints to stacks router
* feat(files): FileTree and FileTreeNode components
* fix(files): route security hardening and stream cleanup
* fix(files): FileTree accessibility, icon stroke, stale fetch guard
Add strokeWidth={1.5} to all Lucide icons in FileTreeNode to match the
design system. Add aria-expanded to directory rows for accessibility.
Guard handleDirClick .then() callbacks against stale stack name
references when the component re-renders with a new stack. Add
toast.info fallbacks when compose.yaml or .env is clicked without a
navigation callback registered.
* feat(files): FileViewer, FileUploadDropzone, NewFolderDialog, DeleteFileConfirm
* fix(files): resolve code quality findings in file explorer components
- Move editorOptions useMemo above conditional returns in FileViewer (Rules of Hooks fix)
- Fix blob download: append anchor to DOM before click, defer URL revoke 100ms
- Keep protected-file confirm input visible during NOT_EMPTY recursive retry in DeleteFileConfirm
- Remove non-functional cursor-pointer/onClick from Community upgrade pill in FileUploadDropzone
- Add success toast on folder creation in NewFolderDialog
- Switch all (e as Error).message casts to instanceof Error narrowing
* test(files): unit tests for binary detection, stack path safety, and file explorer routes
- binary-detection.test.ts: covers isBinaryBuffer edge cases (empty, NUL,
PNG header, threshold boundary, sampleBytes parameter)
- filesystem-stack-paths.test.ts: covers isValidRelativeStackPath (accepts/
rejects matrix) and FileSystemService stack methods against a real temp dir
(listStackDirectory sort and protection flags, readStackFile text/binary/
oversized paths, writeStackFile/Buffer, deleteStackPath, mkdirStackPath,
traversal guard); platform-specific empty-dir/NOT_EMPTY cases skip on Windows
- stack-files-routes.test.ts: route-level integration tests for all seven
file explorer endpoints; covers auth gating, Community-tier 403 gates,
input validation, 413 TOO_LARGE upload limit, and 204/200 happy paths
* feat(files): StackFileExplorer container with lazy tree, viewer, and action bar
* fix(files): add Download button to explorer toolbar, fix Community upgrade pill, reset state on stack change
* test(files): add missing test coverage for file explorer routes and service
* feat(files): add Files tab to EditorLayout with StackFileExplorer integration
* fix(files): add defensive activeTab guard to saveFile and discardChanges
* test(files): unit tests for FileTree expand/collapse and FileViewer render modes
Covers the three FileViewer content modes (text/Monaco, binary panel,
oversized panel) and the FileTree expand/collapse/cache cycle: first
expand fetches the subdirectory, second click collapses without a fetch,
third click re-expands from the in-memory cache without a second fetch.
* test(e2e): file explorer community and skipper+ flows
Covers the full file-explorer feature surface in two describe blocks:
Community (read-only): intercepts /api/license to simulate community
tier, confirms the upgrade pill is visible in the left pane, and
asserts that the Save button is absent after opening a text file.
Skipper+ (full CRUD): uploads a text file and confirms it appears in
the tree; edits config/app.conf and saves via Monaco; deletes an
uploaded file and asserts the tree entry is gone; issues a raw HTTP
request to the download endpoint and checks for status 200 and the
content-disposition: attachment header.
Also adds data-testid="file-action-delete" to the action bar Delete
button in StackFileExplorer for stable targeting, and exports
waitForStacksLoaded from e2e/helpers.ts to eliminate the three
identical local copies in stacks, deploy-log-panel, and stack-files
spec files.
* fix(e2e): improve test isolation and selector stability in stack-files spec
Move beforeEach seed to beforeAll/afterAll so fixtures are created once per
suite, not before every test. Extract shared seedSuite/teardownSuite helpers
to eliminate the duplicate beforeAll/afterAll blocks. Wrap teardown in
try/catch so failures log a warning rather than masking test results.
Replace waitForTimeout(500) with a deterministic expect on the file tree
sentinel. Add data-testid="anatomy-files-btn" and data-testid="delete-confirm-btn"
to replace the fragile button text/positional selectors. Assert Save button
starts disabled before editing.
* docs(files): add stack file explorer documentation
Add user-facing guide for the stack file explorer feature covering
tier access (Community read-only, Skipper+ read-write), viewing
limits, upload/download caps, protected file routing, and
troubleshooting. Update the editor page to reference the new guide
and register the page in the navigation.
* fix(docs): use canonical Skipper tier name in file explorer overview card
* fix(files): resolve lint errors blocking CI
Remove unnecessary backslash escape before double-quote in the
Content-Disposition regex (no-useless-escape). Replace five synchronous
setState resets at the top of the FileTree mount effect with a React key
prop on the FileTree element in StackFileExplorer so remounting resets
state automatically, eliminating the react-hooks/set-state-in-effect
violation.
* test(files): fix e2e seeding to work on community-tier CI
Replace the browser-side paid upload/mkdir API calls in seedTestStack with
direct Node fs writes. The upload and folder endpoints require Skipper+ so
they returned 403 on CI, which runs with no license set. Stack creation
via POST /api/stacks stays as an API call since it is community-allowed and
keeps the backend registry in sync.
Add a per-test tier check in the Skipper+ beforeEach that skips gracefully
when the instance is community, matching the pattern in auto-heal-policies.
114 lines
4.8 KiB
TypeScript
114 lines
4.8 KiB
TypeScript
/**
|
|
* Shared helpers for Sencho E2E tests.
|
|
*
|
|
* CREDENTIALS: Set E2E_USERNAME and E2E_PASSWORD env vars to match
|
|
* your dev instance's admin account. Defaults assume the initial setup
|
|
* was completed with username "admin" and password "password123".
|
|
*
|
|
* E2E_USERNAME=admin E2E_PASSWORD=mypassword npx playwright test
|
|
*/
|
|
import { Page, expect } from '@playwright/test';
|
|
import { authenticator } from 'otplib';
|
|
import { HashAlgorithms } from '@otplib/core';
|
|
|
|
// Match backend configuration so generated codes are accepted.
|
|
authenticator.options = {
|
|
digits: 6,
|
|
step: 30,
|
|
algorithm: HashAlgorithms.SHA1,
|
|
window: 1,
|
|
};
|
|
|
|
export const TEST_USERNAME = process.env.E2E_USERNAME ?? 'admin';
|
|
export const TEST_PASSWORD = process.env.E2E_PASSWORD ?? 'password123';
|
|
|
|
/**
|
|
* Generate a current TOTP code for the given base32 secret. Used by the MFA
|
|
* E2E tests to drive the login challenge without a real authenticator app.
|
|
*/
|
|
export function totpNow(secret: string): string {
|
|
return authenticator.generate(secret.replace(/\s+/g, ''));
|
|
}
|
|
|
|
/** Selector for the dashboard - only present in EditorLayout, not on login/setup pages */
|
|
const DASHBOARD_INDICATOR = 'img[alt="Sencho Logo"]';
|
|
|
|
/** Returns true if the current page is the first-run setup screen */
|
|
async function isSetupPage(page: Page): Promise<boolean> {
|
|
return page.locator('#confirmPassword, input[placeholder*="Confirm"]').isVisible().catch(() => false);
|
|
}
|
|
|
|
/** Returns true if the current page is the login screen */
|
|
async function isLoginPage(page: Page): Promise<boolean> {
|
|
return page.locator('button:has-text("Login"), button:has-text("Sign in")').isVisible().catch(() => false);
|
|
}
|
|
|
|
/** Returns true if the dashboard (EditorLayout) is loaded */
|
|
export async function isDashboard(page: Page): Promise<boolean> {
|
|
return page.locator(DASHBOARD_INDICATOR).isVisible().catch(() => false);
|
|
}
|
|
|
|
/**
|
|
* Wait for the stacks sidebar to finish loading. Waits for the Create Stack
|
|
* button and the data-stacks-loaded sentinel set by the CommandList after its
|
|
* async refreshStacks() call resolves.
|
|
*/
|
|
export async function waitForStacksLoaded(page: Page): Promise<void> {
|
|
await expect(page.getByRole('button', { name: 'Create Stack' })).toBeVisible({ timeout: 15_000 });
|
|
await expect(page.locator('[data-stacks-loaded="true"]')).toBeAttached({ timeout: 15_000 });
|
|
}
|
|
|
|
/**
|
|
* Navigate to the app root, complete first-run setup if needed, then log in.
|
|
* After this call the dashboard is guaranteed to be visible.
|
|
*/
|
|
export async function loginAs(page: Page, username = TEST_USERNAME, password = TEST_PASSWORD) {
|
|
await page.goto('/');
|
|
|
|
// Wait for the app to finish its auth check (loading spinner disappears)
|
|
await page.waitForTimeout(500);
|
|
|
|
// ── First-run setup ───────────────────────────────────────────────────────
|
|
if (await isSetupPage(page)) {
|
|
await page.locator('#username').fill(username);
|
|
await page.locator('#password').fill(password);
|
|
const confirmInput = page.locator('#confirmPassword');
|
|
if (await confirmInput.isVisible()) await confirmInput.fill(password);
|
|
await page.locator('button[type="submit"]').click();
|
|
// After setup, the app logs in automatically and shows the dashboard
|
|
await expect(page.locator(DASHBOARD_INDICATOR)).toBeVisible({ timeout: 10_000 });
|
|
return;
|
|
}
|
|
|
|
// ── Login screen ─────────────────────────────────────────────────────────
|
|
if (await isLoginPage(page)) {
|
|
// Confirm the username field is actually visible before filling. If
|
|
// isLoginPage was a transient false positive (e.g. login form briefly
|
|
// rendered before auth check redirected to dashboard), the fill would
|
|
// hang forever waiting for #username to come back.
|
|
const usernameField = page.locator('#username');
|
|
const usernameVisible = await usernameField
|
|
.waitFor({ state: 'visible', timeout: 2_000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
if (usernameVisible) {
|
|
await usernameField.fill(username);
|
|
await page.locator('#password').fill(password);
|
|
await page.locator('button:has-text("Login"), button:has-text("Sign in")').first().click();
|
|
await expect(page.locator(DASHBOARD_INDICATOR)).toBeVisible({ timeout: 10_000 });
|
|
return;
|
|
}
|
|
// Fall through to the dashboard check below.
|
|
}
|
|
|
|
// ── Already on the dashboard ──────────────────────────────────────────────
|
|
if (await isDashboard(page)) {
|
|
return;
|
|
}
|
|
|
|
throw new Error(
|
|
'loginAs: could not determine page state - expected setup, login, or dashboard. ' +
|
|
'Check that E2E_USERNAME and E2E_PASSWORD are set correctly.',
|
|
);
|
|
}
|