Skip to content

Commit 46c2593

Browse files
authored
chore: fix namespace check (#196)
* chore: fix namespace check * chore: lint * chore: add test
1 parent fb436a9 commit 46c2593

File tree

21 files changed

+785
-70
lines changed

21 files changed

+785
-70
lines changed

.github/workflows/integration-test.yml

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,25 @@ jobs:
161161
- name: Build project
162162
if: steps.cache-build.outputs.cache-hit != 'true'
163163
run: pnpm run build
164-
165-
- name: Start preview server
164+
165+
- name: Setup Bun
166+
uses: oven-sh/setup-bun@v2
167+
with:
168+
bun-version: latest
169+
170+
- name: Install Bun SSR dependencies
171+
run: bun install cheerio pino pino-pretty
172+
173+
- name: Start SSR server
166174
run: |
167-
pnpm run start &
168-
echo $! > preview-server.pid
175+
pnpm run dev:server &
176+
echo $! > ssr-server.pid
169177
170-
# Optimized preview server health check
178+
# SSR server health check
171179
timeout 60 bash -c 'until curl -sf http://localhost:3000 > /dev/null 2>&1; do
172-
echo "Waiting for preview server... ($(date +%T))"
180+
echo "Waiting for SSR server... ($(date +%T))"
173181
sleep 2
174-
done' && echo "✓ Preview server is ready" || (echo "❌ Preview server failed to start" && exit 1)
182+
done' && echo "✓ SSR server is ready" || (echo "❌ SSR server failed to start" && exit 1)
175183
176184
- name: Run ${{ matrix.test-group.name }} tests
177185
run: |
@@ -193,11 +201,11 @@ jobs:
193201
- name: Cleanup
194202
if: always()
195203
run: |
196-
# Kill preview server
197-
if [ -f preview-server.pid ]; then
198-
kill $(cat preview-server.pid) 2>/dev/null || true
204+
# Kill SSR server
205+
if [ -f ssr-server.pid ]; then
206+
kill $(cat ssr-server.pid) 2>/dev/null || true
199207
fi
200-
pkill -f "vite preview" 2>/dev/null || true
208+
pkill -f "bun deploy/server.ts" 2>/dev/null || true
201209
202210
# Stop Docker services
203211
cd AppFlowy-Cloud-Premium && docker compose down || true

.storybook/GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ Many components behave differently based on whether they're running on official
280280

281281
### How It Works
282282

283-
The `isOfficialHost()` function in `src/utils/subscription.ts` checks `window.location.hostname`. For Storybook, we mock this using a global variable.
283+
The `isAppFlowyHosted()` function in `src/utils/subscription.ts` checks `window.location.hostname`. For Storybook, we mock this using a global variable.
284284

285285
### Using Shared Hostname Decorators
286286

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { AuthTestUtils } from '../../support/auth-utils';
2+
import { TestTool } from '../../support/page-utils';
3+
import { ShareSelectors, SidebarSelectors, PageSelectors } from '../../support/selectors';
4+
import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config';
5+
import { testLog } from '../../support/test-helpers';
6+
7+
describe('Publish Manage - Subscription and Namespace Tests', () => {
8+
let testEmail: string;
9+
10+
before(() => {
11+
logAppFlowyEnvironment();
12+
});
13+
14+
beforeEach(() => {
15+
testEmail = generateRandomEmail();
16+
17+
// Handle uncaught exceptions
18+
cy.on('uncaught:exception', (err: Error) => {
19+
if (
20+
err.message.includes('No workspace or service found') ||
21+
err.message.includes('createThemeNoVars_default is not a function') ||
22+
err.message.includes('View not found') ||
23+
err.message.includes('Record not found') ||
24+
err.message.includes('Request failed') ||
25+
err.name === 'NotAllowedError'
26+
) {
27+
return false;
28+
}
29+
30+
return true;
31+
});
32+
});
33+
34+
/**
35+
* Helper to sign in, publish a page, and open the publish manage panel
36+
*/
37+
const setupPublishManagePanel = (email: string) => {
38+
cy.visit('/login', { failOnStatusCode: false });
39+
cy.wait(1000);
40+
const authUtils = new AuthTestUtils();
41+
42+
return authUtils.signInWithTestUrl(email).then(() => {
43+
cy.url().should('include', '/app');
44+
testLog.info('Signed in');
45+
46+
// Wait for app to fully load
47+
SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 });
48+
PageSelectors.names().should('exist', { timeout: 30000 });
49+
cy.wait(2000);
50+
51+
// Publish a page
52+
TestTool.openSharePopover();
53+
cy.contains('Publish').should('exist').click({ force: true });
54+
cy.wait(1000);
55+
56+
ShareSelectors.publishConfirmButton().should('be.visible').should('not.be.disabled');
57+
ShareSelectors.publishConfirmButton().click({ force: true });
58+
testLog.info('Clicked Publish button');
59+
60+
// Wait for publish to complete
61+
cy.wait(5000);
62+
ShareSelectors.publishNamespace().should('be.visible', { timeout: 10000 });
63+
testLog.info('Page published successfully');
64+
65+
// Open the publish settings (manage panel)
66+
ShareSelectors.openPublishSettingsButton().should('be.visible').click({ force: true });
67+
cy.wait(2000);
68+
ShareSelectors.publishManagePanel().should('be.visible', { timeout: 10000 });
69+
testLog.info('Publish manage panel is visible');
70+
});
71+
};
72+
73+
it('should hide homepage setting when namespace is UUID (new users)', () => {
74+
// New users have UUID namespaces by default
75+
// The HomePageSetting component returns null when canEdit is false (UUID namespace)
76+
setupPublishManagePanel(testEmail).then(() => {
77+
// Wait for the panel content to fully render
78+
cy.wait(1000);
79+
80+
// Verify that homepage setting is NOT visible when namespace is a UUID
81+
// New users have UUID namespaces, so the homepage setting should be hidden
82+
ShareSelectors.publishManagePanel().within(() => {
83+
cy.get('[data-testid="homepage-setting"]').should('not.exist');
84+
testLog.info('✓ Homepage setting is correctly hidden for UUID namespace');
85+
86+
// The edit namespace button should still exist (it's always rendered)
87+
cy.get('[data-testid="edit-namespace-button"]').should('exist');
88+
testLog.info('✓ Edit namespace button exists');
89+
});
90+
91+
// Close the modal
92+
cy.get('body').type('{esc}');
93+
cy.wait(500);
94+
});
95+
});
96+
97+
it('edit namespace button should be visible but clicking does nothing for Free plan on official host', () => {
98+
// This test verifies the subscription check:
99+
// - On official hosts (including localhost in dev): Free plan users see the button but clicking does nothing
100+
// - The button is rendered but the onClick handler returns early
101+
setupPublishManagePanel(testEmail).then(() => {
102+
cy.wait(1000);
103+
104+
ShareSelectors.publishManagePanel().within(() => {
105+
// The edit namespace button should exist
106+
cy.get('[data-testid="edit-namespace-button"]').should('exist').as('editBtn');
107+
testLog.info('Edit namespace button exists');
108+
109+
// Click the button - on official hosts with Free plan, nothing should happen
110+
// The UpdateNamespace modal should NOT open
111+
cy.get('@editBtn').click({ force: true });
112+
});
113+
114+
// Wait a moment for any modal to potentially appear
115+
cy.wait(1000);
116+
117+
// The UpdateNamespace dialog should NOT appear because:
118+
// 1. User is on Free plan
119+
// 2. localhost is treated as official host (isAppFlowyHosted returns true)
120+
// The modal has class 'MuiDialog-root' or similar - check it doesn't exist
121+
cy.get('body').then(($body) => {
122+
// Look for any modal that might be the namespace update dialog
123+
const hasNamespaceModal = $body.find('[role="dialog"]').filter((_, el) => {
124+
return el.textContent?.includes('Update namespace') || el.textContent?.includes('Namespace');
125+
}).length > 0;
126+
127+
if (!hasNamespaceModal) {
128+
testLog.info('✓ Edit namespace dialog correctly blocked (Free plan on official host)');
129+
} else {
130+
// If modal appeared, this might be a self-hosted environment where check is skipped
131+
testLog.info('Note: Namespace dialog appeared - may be self-hosted environment');
132+
}
133+
});
134+
135+
// Close any open dialogs
136+
cy.get('body').type('{esc}');
137+
cy.wait(500);
138+
});
139+
});
140+
141+
it('namespace URL button should be clickable even with UUID namespace', () => {
142+
// Verify that the namespace URL can be clicked/visited regardless of UUID status
143+
setupPublishManagePanel(testEmail).then(() => {
144+
cy.wait(1000);
145+
146+
// Find the namespace URL button and verify it's clickable
147+
// The button should not be disabled even for UUID namespaces
148+
ShareSelectors.publishManagePanel().within(() => {
149+
// Find any button that contains the namespace link (has '/' in text)
150+
cy.get('button').contains('/').should('be.visible').should('not.be.disabled');
151+
testLog.info('✓ Namespace URL button is visible and clickable');
152+
});
153+
154+
// Close the modal
155+
cy.get('body').type('{esc}');
156+
cy.wait(500);
157+
});
158+
});
159+
160+
it('should allow namespace edit on self-hosted (non-official) environments', () => {
161+
// This test simulates a self-hosted environment where subscription checks are skipped
162+
// We use localStorage to override the isAppFlowyHosted() check
163+
164+
// Set up the override BEFORE visiting the page
165+
cy.visit('/login', { failOnStatusCode: false });
166+
cy.window().then((win) => {
167+
win.localStorage.setItem('__test_force_self_hosted', 'true');
168+
});
169+
170+
cy.wait(500);
171+
const authUtils = new AuthTestUtils();
172+
173+
authUtils.signInWithTestUrl(testEmail).then(() => {
174+
cy.url().should('include', '/app');
175+
testLog.info('Signed in (self-hosted mode)');
176+
177+
// Wait for app to fully load
178+
SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 });
179+
PageSelectors.names().should('exist', { timeout: 30000 });
180+
cy.wait(2000);
181+
182+
// Publish a page
183+
TestTool.openSharePopover();
184+
cy.contains('Publish').should('exist').click({ force: true });
185+
cy.wait(1000);
186+
187+
ShareSelectors.publishConfirmButton().should('be.visible').should('not.be.disabled');
188+
ShareSelectors.publishConfirmButton().click({ force: true });
189+
testLog.info('Clicked Publish button');
190+
191+
// Wait for publish to complete
192+
cy.wait(5000);
193+
ShareSelectors.publishNamespace().should('be.visible', { timeout: 10000 });
194+
testLog.info('Page published successfully');
195+
196+
// Open the publish settings (manage panel)
197+
ShareSelectors.openPublishSettingsButton().should('be.visible').click({ force: true });
198+
cy.wait(2000);
199+
ShareSelectors.publishManagePanel().should('be.visible', { timeout: 10000 });
200+
testLog.info('Publish manage panel is visible');
201+
202+
// On self-hosted, clicking the edit button should open the dialog (no subscription check)
203+
// Since user is owner, the edit should work
204+
ShareSelectors.publishManagePanel().within(() => {
205+
cy.get('[data-testid="edit-namespace-button"]').should('exist').click({ force: true });
206+
});
207+
208+
// Wait and check if the namespace update dialog appears
209+
// On self-hosted, it should open since we only check owner status (not subscription)
210+
cy.wait(1000);
211+
212+
// The dialog should appear on self-hosted environments
213+
// Look for the namespace update dialog
214+
cy.get('body').then(($body) => {
215+
const hasDialog = $body.find('[role="dialog"]').length > 0;
216+
217+
if (hasDialog) {
218+
testLog.info('✓ Namespace edit dialog opened on self-hosted environment');
219+
// Close the dialog
220+
cy.get('body').type('{esc}');
221+
} else {
222+
testLog.info('Note: Dialog did not open - this may indicate the owner check failed');
223+
}
224+
});
225+
226+
// Clean up: remove the override
227+
cy.window().then((win) => {
228+
win.localStorage.removeItem('__test_force_self_hosted');
229+
});
230+
231+
// Close any remaining modals
232+
cy.get('body').type('{esc}');
233+
cy.wait(500);
234+
});
235+
});
236+
});

cypress/support/selectors.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,15 @@ export const ShareSelectors = {
242242
visitSiteButton: () => cy.get(byTestId('visit-site-button')),
243243
publishManageModal: () => cy.get(byTestId('publish-manage-modal')),
244244
publishManagePanel: () => cy.get(byTestId('publish-manage-panel')),
245+
246+
// Edit namespace button
247+
editNamespaceButton: () => cy.get(byTestId('edit-namespace-button')),
248+
249+
// Homepage setting (only visible when namespace is not a UUID)
250+
homePageSetting: () => cy.get(byTestId('homepage-setting')),
251+
252+
// Homepage upgrade button (visible for Free plan users on official hosts)
253+
homePageUpgradeButton: () => cy.get(byTestId('homepage-upgrade-button')),
245254
};
246255

247256
/**

deploy/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import path from 'path';
2+
import fs from 'fs';
23

3-
export const distDir = path.join(__dirname, 'dist');
4+
// In production, dist is copied to deploy/dist. In dev, it's at project root.
5+
const prodDistDir = path.join(__dirname, 'dist');
6+
const devDistDir = path.join(__dirname, '..', 'dist');
7+
8+
export const distDir = fs.existsSync(prodDistDir) ? prodDistDir : devDistDir;
49
export const indexPath = path.join(distDir, 'index.html');
510
export const baseURL = process.env.APPFLOWY_BASE_URL as string;
611
// Used when a namespace is requested without /publishName; users get redirected to the

0 commit comments

Comments
 (0)