fix(web): convert favicon.ico to RGBA format for Turbopack (#544)
All checks were successful
ci/woodpecker/push/web Pipeline was successful
All checks were successful
ci/woodpecker/push/web Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #544.
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 521 B After Width: | Height: | Size: 539 B |
@@ -691,4 +691,175 @@ describe("AuthContext", (): void => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("workspace ID persistence", (): void => {
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// localStorage mock for workspace persistence tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
interface MockLocalStorage {
|
||||||
|
getItem: ReturnType<typeof vi.fn>;
|
||||||
|
setItem: ReturnType<typeof vi.fn>;
|
||||||
|
removeItem: ReturnType<typeof vi.fn>;
|
||||||
|
clear: ReturnType<typeof vi.fn>;
|
||||||
|
readonly length: number;
|
||||||
|
key: ReturnType<typeof vi.fn>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let localStorageMock: MockLocalStorage;
|
||||||
|
|
||||||
|
beforeEach((): void => {
|
||||||
|
let store: Record<string, string> = {};
|
||||||
|
localStorageMock = {
|
||||||
|
getItem: vi.fn((key: string): string | null => store[key] ?? null),
|
||||||
|
setItem: vi.fn((key: string, value: string): void => {
|
||||||
|
store[key] = value;
|
||||||
|
}),
|
||||||
|
removeItem: vi.fn((key: string): void => {
|
||||||
|
store = Object.fromEntries(Object.entries(store).filter(([k]) => k !== key));
|
||||||
|
}),
|
||||||
|
clear: vi.fn((): void => {
|
||||||
|
store = {};
|
||||||
|
}),
|
||||||
|
get length(): number {
|
||||||
|
return Object.keys(store).length;
|
||||||
|
},
|
||||||
|
key: vi.fn((_index: number): string | null => null),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(window, "localStorage", {
|
||||||
|
value: localStorageMock,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach((): void => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should persist currentWorkspaceId to localStorage after session check", async (): Promise<void> => {
|
||||||
|
const mockUser: AuthUser = {
|
||||||
|
id: "user-1",
|
||||||
|
email: "test@example.com",
|
||||||
|
name: "Test User",
|
||||||
|
currentWorkspaceId: "ws-current-123",
|
||||||
|
workspaceId: "ws-default-456",
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(apiGet).mockResolvedValueOnce({
|
||||||
|
user: mockUser,
|
||||||
|
session: { id: "session-1", token: "token123", expiresAt: futureExpiry() },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AuthProvider>
|
||||||
|
<TestComponent />
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("auth-status")).toHaveTextContent("Authenticated");
|
||||||
|
});
|
||||||
|
|
||||||
|
// currentWorkspaceId takes priority over workspaceId
|
||||||
|
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||||
|
"mosaic-workspace-id",
|
||||||
|
"ws-current-123"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to workspaceId when currentWorkspaceId is absent", async (): Promise<void> => {
|
||||||
|
const mockUser: AuthUser = {
|
||||||
|
id: "user-1",
|
||||||
|
email: "test@example.com",
|
||||||
|
name: "Test User",
|
||||||
|
workspaceId: "ws-default-456",
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(apiGet).mockResolvedValueOnce({
|
||||||
|
user: mockUser,
|
||||||
|
session: { id: "session-1", token: "token123", expiresAt: futureExpiry() },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AuthProvider>
|
||||||
|
<TestComponent />
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("auth-status")).toHaveTextContent("Authenticated");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||||
|
"mosaic-workspace-id",
|
||||||
|
"ws-default-456"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not write to localStorage when no workspace ID is present on user", async (): Promise<void> => {
|
||||||
|
const mockUser: AuthUser = {
|
||||||
|
id: "user-1",
|
||||||
|
email: "test@example.com",
|
||||||
|
name: "Test User",
|
||||||
|
// no workspaceId or currentWorkspaceId
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(apiGet).mockResolvedValueOnce({
|
||||||
|
user: mockUser,
|
||||||
|
session: { id: "session-1", token: "token123", expiresAt: futureExpiry() },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AuthProvider>
|
||||||
|
<TestComponent />
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("auth-status")).toHaveTextContent("Authenticated");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(localStorageMock.setItem).not.toHaveBeenCalledWith(
|
||||||
|
"mosaic-workspace-id",
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove workspace ID from localStorage on sign-out", async (): Promise<void> => {
|
||||||
|
const mockUser: AuthUser = {
|
||||||
|
id: "user-1",
|
||||||
|
email: "test@example.com",
|
||||||
|
name: "Test User",
|
||||||
|
currentWorkspaceId: "ws-current-123",
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(apiGet).mockResolvedValueOnce({
|
||||||
|
user: mockUser,
|
||||||
|
session: { id: "session-1", token: "token123", expiresAt: futureExpiry() },
|
||||||
|
});
|
||||||
|
vi.mocked(apiPost).mockResolvedValueOnce({ success: true });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AuthProvider>
|
||||||
|
<TestComponent />
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("auth-status")).toHaveTextContent("Authenticated");
|
||||||
|
});
|
||||||
|
|
||||||
|
const signOutButton = screen.getByRole("button", { name: "Sign Out" });
|
||||||
|
signOutButton.click();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("auth-status")).toHaveTextContent("Not Authenticated");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(localStorageMock.removeItem).toHaveBeenCalledWith("mosaic-workspace-id");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,43 @@ const SESSION_EXPIRY_WARNING_MINUTES = 5;
|
|||||||
|
|
||||||
/** Interval in milliseconds to check session expiry */
|
/** Interval in milliseconds to check session expiry */
|
||||||
const SESSION_CHECK_INTERVAL_MS = 60_000;
|
const SESSION_CHECK_INTERVAL_MS = 60_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* localStorage key for the active workspace ID.
|
||||||
|
* Must match the WORKSPACE_KEY constant in useLayout.ts and the key read
|
||||||
|
* by apiRequest in client.ts.
|
||||||
|
*/
|
||||||
|
const WORKSPACE_STORAGE_KEY = "mosaic-workspace-id";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist the workspace ID to localStorage so it is available to
|
||||||
|
* useWorkspaceId and apiRequest on the next render / request cycle.
|
||||||
|
* Silently ignores localStorage errors (private browsing, storage full).
|
||||||
|
*/
|
||||||
|
function persistWorkspaceId(workspaceId: string | undefined): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
if (workspaceId) {
|
||||||
|
localStorage.setItem(WORKSPACE_STORAGE_KEY, workspaceId);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// localStorage unavailable — not fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the workspace ID from localStorage on sign-out so stale workspace
|
||||||
|
* context is not sent on subsequent unauthenticated requests.
|
||||||
|
*/
|
||||||
|
function clearWorkspaceId(): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(WORKSPACE_STORAGE_KEY);
|
||||||
|
} catch {
|
||||||
|
// localStorage unavailable — not fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const MOCK_AUTH_USER: AuthUser = {
|
const MOCK_AUTH_USER: AuthUser = {
|
||||||
id: "dev-user-local",
|
id: "dev-user-local",
|
||||||
email: "dev@localhost",
|
email: "dev@localhost",
|
||||||
@@ -97,6 +134,11 @@ function RealAuthProvider({ children }: { children: ReactNode }): React.JSX.Elem
|
|||||||
setUser(session.user);
|
setUser(session.user);
|
||||||
setAuthError(null);
|
setAuthError(null);
|
||||||
|
|
||||||
|
// Persist workspace ID to localStorage so useWorkspaceId and apiRequest
|
||||||
|
// can pick it up without re-fetching the session.
|
||||||
|
// Prefer currentWorkspaceId (the user's active workspace) over workspaceId.
|
||||||
|
persistWorkspaceId(session.user.currentWorkspaceId ?? session.user.workspaceId);
|
||||||
|
|
||||||
// Track session expiry timestamp
|
// Track session expiry timestamp
|
||||||
expiresAtRef.current = new Date(session.session.expiresAt);
|
expiresAtRef.current = new Date(session.session.expiresAt);
|
||||||
|
|
||||||
@@ -128,6 +170,9 @@ function RealAuthProvider({ children }: { children: ReactNode }): React.JSX.Elem
|
|||||||
setUser(null);
|
setUser(null);
|
||||||
expiresAtRef.current = null;
|
expiresAtRef.current = null;
|
||||||
setSessionExpiring(false);
|
setSessionExpiring(false);
|
||||||
|
// Clear persisted workspace ID so stale context is not sent on
|
||||||
|
// subsequent unauthenticated API requests.
|
||||||
|
clearWorkspaceId();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user