Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions surfsense_backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ dependencies = [
[dependency-groups]
dev = [
"ruff>=0.12.5",
"pytest>=8.3.0",
"pytest-asyncio>=0.24.0",
"pytest-cov>=6.0.0",
"httpx>=0.28.0",
]

[tool.ruff]
Expand Down
15 changes: 15 additions & 0 deletions surfsense_backend/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[pytest]
testpaths = tests
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The asyncio_default_fixture_loop_scope configuration option is deprecated in pytest-asyncio 0.23.0+. With pytest-asyncio>=0.24.0 specified in dependencies, consider using asyncio_fixture_scope instead, or rely on the default function scope which is already the standard behavior.

Suggested change
asyncio_default_fixture_loop_scope = function

Copilot uses AI. Check for mistakes.
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
markers =
unit: Unit tests (fast, isolated)
integration: Integration tests (may require external services)
slow: Slow running tests
1 change: 1 addition & 0 deletions surfsense_backend/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Test package for SurfSense Backend
69 changes: 69 additions & 0 deletions surfsense_backend/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
Shared test fixtures and configuration for SurfSense Backend tests.
"""

import uuid
from unittest.mock import AsyncMock, MagicMock

import pytest
from sqlalchemy.ext.asyncio import AsyncSession


@pytest.fixture
def mock_session() -> AsyncMock:
"""Create a mock async database session."""
session = AsyncMock(spec=AsyncSession)
session.execute = AsyncMock()
session.commit = AsyncMock()
session.rollback = AsyncMock()
session.refresh = AsyncMock()
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The session.add method is mocked as a synchronous MagicMock() while other session methods are AsyncMock(). In SQLAlchemy, session.add() is synchronous, but for consistency and to avoid confusion, consider documenting why this is different or using MagicMock() explicitly with a comment explaining the synchronous nature.

Suggested change
session.refresh = AsyncMock()
session.refresh = AsyncMock()
# Note: session.add() is synchronous even on AsyncSession, so we use MagicMock() here.

Copilot uses AI. Check for mistakes.
session.add = MagicMock()
session.delete = AsyncMock()
return session


@pytest.fixture
def mock_user() -> MagicMock:
"""Create a mock user object."""
user = MagicMock()
user.id = uuid.uuid4()
user.email = "test@example.com"
user.is_active = True
user.is_superuser = False
user.is_verified = True
return user


@pytest.fixture
def mock_search_space() -> MagicMock:
"""Create a mock search space object."""
search_space = MagicMock()
search_space.id = 1
search_space.name = "Test Search Space"
search_space.llm_configs = []
search_space.fast_llm_id = None
search_space.long_context_llm_id = None
search_space.strategic_llm_id = None
return search_space


@pytest.fixture
def sample_messages() -> list[dict]:
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using more specific type hints. Instead of list[dict], use list[dict[str, str]] to better specify the structure of chat messages with role and content fields.

Suggested change
def sample_messages() -> list[dict]:
def sample_messages() -> list[dict[str, str]]:

Copilot uses AI. Check for mistakes.
"""Sample chat messages for testing."""
return [
{"role": "user", "content": "Hello, how are you?"},
{"role": "assistant", "content": "I'm doing well, thank you!"},
{"role": "user", "content": "What is the weather today?"},
]
Comment on lines +53 to +57
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sample_messages fixture returns a mutable list that could be modified by tests, potentially affecting subsequent tests. Consider using @pytest.fixture(scope="function") explicitly or returning a new copy of the list each time to ensure test isolation. Alternatively, document that tests should not mutate this fixture data.

Suggested change
return [
{"role": "user", "content": "Hello, how are you?"},
{"role": "assistant", "content": "I'm doing well, thank you!"},
{"role": "user", "content": "What is the weather today?"},
]
messages = [
{"role": "user", "content": "Hello, how are you?"},
{"role": "assistant", "content": "I'm doing well, thank you!"},
{"role": "user", "content": "What is the weather today?"},
]
return messages.copy()

Copilot uses AI. Check for mistakes.


@pytest.fixture
def sample_chat_create_data() -> dict:
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a more specific type hint like dict[str, Any] or creating a TypedDict for the chat creation data structure to provide better type safety and documentation.

Copilot uses AI. Check for mistakes.
"""Sample data for creating a chat."""
return {
"title": "Test Chat",
"type": "normal",
"search_space_id": 1,
"initial_connectors": [],
"messages": [],
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to sample_messages, the sample_chat_create_data fixture returns a mutable dictionary. Consider returning a copy (return {...}) or documenting that tests should not mutate the returned data to prevent potential test interference.

Suggested change
}
}.copy()

Copilot uses AI. Check for mistakes.
13 changes: 11 additions & 2 deletions surfsense_web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"format:fix": "npx @biomejs/biome check --fix"
"format:fix": "npx @biomejs/biome check --fix",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@ai-sdk/react": "^1.2.12",
Expand Down Expand Up @@ -103,17 +106,23 @@
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/typography": "^0.5.16",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
Comment on lines +109 to +110
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider adding @testing-library/user-event as a devDependency. It provides a more complete and realistic way to simulate user interactions in tests compared to using fireEvent directly, following React Testing Library best practices.

Copilot uses AI. Check for mistakes.
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^20.19.9",
"@types/pg": "^8.15.5",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^2.1.8",
"cross-env": "^7.0.3",
"drizzle-kit": "^0.31.5",
"eslint": "^9.32.0",
"eslint-config-next": "15.2.0",
"jsdom": "^25.0.1",
"tailwindcss": "^4.1.11",
"tsx": "^4.20.6",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"vitest": "^2.1.8"
}
}
130 changes: 130 additions & 0 deletions surfsense_web/tests/setup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* Vitest test setup file.
*
* This file runs before all tests and sets up the testing environment.
*/

import "@testing-library/jest-dom/vitest";
import { vi } from "vitest";

// Mock localStorage for auth-utils tests
const localStorageMock = {
store: {} as Record<string, string>,
getItem: vi.fn((key: string) => localStorageMock.store[key] ?? null),
setItem: vi.fn((key: string, value: string) => {
localStorageMock.store[key] = value;
}),
removeItem: vi.fn((key: string) => {
delete localStorageMock.store[key];
}),
clear: vi.fn(() => {
localStorageMock.store = {};
}),
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localStorage mock is missing the length property and key(index) method, which are part of the Storage interface. This could cause tests to fail if code attempts to iterate over localStorage keys using these standard methods. Consider adding:

length: 0,
key: vi.fn((index: number) => {
  const keys = Object.keys(localStorageMock.store);
  return keys[index] ?? null;
}),

And update the length in setItem/removeItem/clear methods.

Suggested change
}),
}),
get length() {
return Object.keys(localStorageMock.store).length;
},
key: vi.fn((index: number) => {
const keys = Object.keys(localStorageMock.store);
return keys[index] ?? null;
}),

Copilot uses AI. Check for mistakes.
};

Object.defineProperty(window, "localStorage", {
value: localStorageMock,
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localStorage mock is missing the writable property in the Object.defineProperty configuration. Consider adding writable: true to allow tests to potentially reassign the mock if needed, maintaining consistency with the window.location mock pattern (line 37).

Suggested change
value: localStorageMock,
value: localStorageMock,
writable: true,

Copilot uses AI. Check for mistakes.
});

// Mock window.location
const locationMock = {
href: "",
pathname: "/dashboard",
search: "",
hash: "",
};

Object.defineProperty(window, "location", {
value: locationMock,
writable: true,
});

// Mock Next.js router
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
}),
usePathname: () => "/",
useSearchParams: () => new URLSearchParams(),
useParams: () => ({}),
}));
Comment on lines +43 to +54
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Next.js router mock returns static mock functions, but in the afterEach cleanup (line 124), only vi.clearAllMocks() is called without resetting the router state. Consider documenting that tests requiring specific router behavior should use vi.mocked() to access and configure these mocks, or add explicit router mock reset logic if tests need isolated router state.

Copilot uses AI. Check for mistakes.

// Mock Next.js Image component
vi.mock("next/image", () => ({
default: ({
src,
alt,
className,
...props
}: {
src: string;
alt: string;
className?: string;
[key: string]: unknown;
}) => {
// eslint-disable-next-line @next/next/no-img-element
return <img src={src} alt={alt} className={className} {...props} />;
},
}));

// Mock Next.js Link component
vi.mock("next/link", () => ({
default: ({
children,
href,
...props
}: {
children: React.ReactNode;
href: string;
[key: string]: unknown;
}) => {
return (
<a href={href} {...props}>
{children}
</a>
);
},
}));

// Mock window.matchMedia for responsive components
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});

// Mock ResizeObserver
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));

// Mock IntersectionObserver
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));

// Clean up after each test
afterEach(() => {
vi.clearAllMocks();
localStorageMock.clear();
locationMock.href = "";
locationMock.pathname = "/dashboard";
locationMock.search = "";
locationMock.hash = "";
});
30 changes: 30 additions & 0 deletions surfsense_web/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "node:path";

export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./tests/setup.tsx"],
include: ["./tests/**/*.{test,spec}.{ts,tsx}"],
exclude: ["node_modules", ".next", "out"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html", "lcov"],
include: ["lib/**/*.{ts,tsx}", "hooks/**/*.{ts,tsx}"],
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coverage configuration only includes lib/** and hooks/** but excludes components/**. This may result in incomplete test coverage visibility for the frontend codebase. Consider adding "components/**/*.{ts,tsx}" to the include array to track coverage for UI components as well.

Suggested change
include: ["lib/**/*.{ts,tsx}", "hooks/**/*.{ts,tsx}"],
include: ["lib/**/*.{ts,tsx}", "hooks/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],

Copilot uses AI. Check for mistakes.
exclude: ["**/*.d.ts", "**/*.test.{ts,tsx}", "**/node_modules/**"],
},
testTimeout: 10000,
},
css: {
// Disable PostCSS for tests
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Setting PostCSS to an empty object effectively disables CSS processing in tests. While this can speed up tests, it may hide CSS-related issues. Consider documenting this decision or evaluating if CSS processing should be enabled for certain integration tests.

Suggested change
// Disable PostCSS for tests
// PostCSS is intentionally disabled for tests to speed up execution and avoid flakiness.
// Note: This may hide CSS-related issues (e.g., missing PostCSS transformations).
// If you have integration or end-to-end tests that rely on CSS, consider enabling PostCSS for those.

Copilot uses AI. Check for mistakes.
postcss: {},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./"),
},
},
});