Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7054511
feat(cli): scaffold @neoboard/cli package with commander.js
alfredorubin96 Apr 2, 2026
dcae245
fix: add product tagline to login and signup pages (#323)
alfredorubin96 Apr 2, 2026
f7696dc
feat(cli): implement all CLI commands with Vitest test suite
alfredorubin96 Apr 2, 2026
2663206
chore(cli): add CLI package to CI pipeline and SonarCloud config
alfredorubin96 Apr 2, 2026
d63cbe9
fix(cli): remove unused imports flagged by eslint
alfredorubin96 Apr 2, 2026
489bcc4
fix(connectors): clear query on type switch, pre-fill edit dialog (#3…
alfredorubin96 Apr 2, 2026
60b7179
fix(auth): registration toggle, API key errors, settings redirect, si…
alfredorubin96 Apr 2, 2026
261e9ac
feat(widgets): widget editor UX improvements (#329, #330, #331, #341,…
alfredorubin96 Apr 3, 2026
e1c43b8
Merge branch 'release/1.0' into fix/issue-325-326-connectors
alfredorubin96 Apr 3, 2026
21c3335
Merge branch 'release/1.0' into fix/issue-324-327-328-340-auth
alfredorubin96 Apr 3, 2026
5f439c6
Merge branch 'release/1.0' into fix/issue-329-343-widget-editor-ux
alfredorubin96 Apr 3, 2026
b950d70
test: add coverage for auth bootstrap-status and API key routes (#346)
alfredorubin96 Apr 3, 2026
004351a
test: add coverage for connector fixes to meet SonarCloud gate (#345)
alfredorubin96 Apr 3, 2026
c0a5a08
test: add coverage for widget editor UX improvements (#347)
alfredorubin96 Apr 3, 2026
e8edf6a
test: add coverage for auth and settings UI components (#346)
alfredorubin96 Apr 3, 2026
89f3831
test: add component tests for widget editor UX features (#347)
alfredorubin96 Apr 3, 2026
8bcedbc
Merge remote-tracking branch 'origin/release/1.0' into feat/cli-scaffold
alfredorubin96 Apr 3, 2026
5e25871
test: add final coverage for connector fixes (#345)
alfredorubin96 Apr 3, 2026
52e0e00
fix(cli): suppress SonarCloud false positive for dev-only default pas…
alfredorubin96 Apr 3, 2026
2d125b6
fix(cli): suppress SonarCloud execSync security hotspot
alfredorubin96 Apr 3, 2026
972b3f6
refactor: extract query templates utility for testability (#347)
alfredorubin96 Apr 3, 2026
2f5d015
revert: remove NOSONAR comments from CLI lib files
alfredorubin96 Apr 3, 2026
d5796a6
Merge branch 'fix/323-login-tagline' into release/1.0-all-fixes
alfredorubin96 Apr 3, 2026
728ea27
Merge remote-tracking branch 'origin/fix/issue-325-326-connectors' in…
alfredorubin96 Apr 3, 2026
8e83ca8
Merge remote-tracking branch 'origin/fix/issue-324-327-328-340-auth' …
alfredorubin96 Apr 3, 2026
807dfb9
Merge remote-tracking branch 'origin/fix/issue-329-343-widget-editor-…
alfredorubin96 Apr 3, 2026
af6864c
fix: resolve duplicate text matches in transform editor test after #3…
alfredorubin96 Apr 3, 2026
2e8f9a4
fix: make settings profile E2E test resilient to role badge matching
alfredorubin96 Apr 3, 2026
8d44927
test: add E2E for settings redirect and auth formatting cleanup
alfredorubin96 Apr 3, 2026
6878c02
test: add E2E for widget no-connector warning, auto-preview, and no-c…
alfredorubin96 Apr 3, 2026
f9b3069
Merge branch 'release/1.0-all-fixes' of https://github.com/alfredo199…
alfredorubin96 Apr 3, 2026
c2b9e48
test: add E2E for connection edit pre-fill and user role change
alfredorubin96 Apr 3, 2026
b8ff2c9
Merge branch 'release/1.0-all-fixes' of https://github.com/alfredo199…
alfredorubin96 Apr 3, 2026
6ecf46c
fix: make force password change tests serial, fix profile info assert…
alfredorubin96 Apr 3, 2026
caaae2b
Merge branch 'release/1.0-all-fixes' of https://github.com/alfredo199…
alfredorubin96 Apr 3, 2026
f536cb0
fix: force password change E2E test — navigate to trigger proxy redirect
alfredorubin96 Apr 3, 2026
25d7d48
Merge remote-tracking branch 'origin/feat/cli-scaffold' into release/…
alfredorubin96 Apr 3, 2026
afecfe9
fix: address CodeRabbit review comments on PR #349
alfredorubin96 Apr 3, 2026
11b486e
fix: refactor CLI exec to use execFileSync for docker commands (Sonar…
alfredorubin96 Apr 3, 2026
9728f91
Merge branch 'release/1.0-all-fixes' of https://github.com/alfredo199…
alfredorubin96 Apr 3, 2026
097bf30
fix: skip force-password-change E2E on CI (JWT timing flake), improve…
alfredorubin96 Apr 3, 2026
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@ ADMIN_BOOTSTRAP_TOKEN=
# Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
API_KEY_HMAC_SECRET=

# Self-registration toggle — set to "false" to disable /signup (optional, default: true)
# REGISTRATION_ENABLED=true

# Tenant ID — defaults to "default" if unset (optional)
# TENANT_ID=default
16 changes: 5 additions & 11 deletions app/e2e/charts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1224,21 +1224,15 @@ test.describe("Column mapping overlay", () => {
timeout: 10_000,
});

// The column mapping overlay should be visible on the grid in edit mode
// Column mapping overlay should NOT appear on dashboard cards (#331)
await expect(
page.locator("[data-testid='column-mapping-overlay']").first(),
).toBeVisible({ timeout: 15_000 });

// X and Y triggers should be present
await expect(
page.locator("[data-testid='column-mapping-x-trigger']").first(),
).toBeVisible();
await expect(
page.locator("[data-testid='column-mapping-y-trigger']").first(),
).toBeVisible();
).not.toBeVisible({ timeout: 5_000 });
});

test("changing axis mapping updates chart", async ({ page }) => {
// Column mapping overlay removed from dashboard cards (#331) — axis mapping
// is now only available inside the widget editor modal.
test.skip("changing axis mapping updates chart", async ({ page }) => {
test.setTimeout(60_000);
await page.getByRole("button", { name: "Add Widget" }).first().click();
const dialog = page.getByRole("dialog", { name: "Add Widget" });
Expand Down
228 changes: 228 additions & 0 deletions app/src/app/(auth)/login/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";

/* ---------- mocks ---------- */

const mockPush = vi.fn();
const mockSignIn = vi.fn();

vi.mock("next-auth/react", () => ({
signIn: (...args: unknown[]) => mockSignIn(...args),
}));

vi.mock("next/navigation", () => ({
useRouter: () => ({ push: mockPush }),
useSearchParams: () => new URLSearchParams(),
}));

vi.mock("next/link", () => ({
__esModule: true,
default: ({
href,
children,
...rest
}: {
href: string;
children: React.ReactNode;
}) => (
<a href={href} {...rest}>
{children}
</a>
),
}));

vi.mock("@neoboard/components", () => ({
Card: ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => <div className={className}>{children}</div>,
CardContent: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
CardDescription: ({ children }: { children: React.ReactNode }) => (
<p>{children}</p>
),
CardFooter: ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => <div className={className}>{children}</div>,
CardHeader: ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => <div className={className}>{children}</div>,
CardTitle: ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => <h2 className={className}>{children}</h2>,
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) => (
<input {...props} />
),
Label: ({
children,
htmlFor,
}: {
children: React.ReactNode;
htmlFor?: string;
}) => <label htmlFor={htmlFor}>{children}</label>,
Alert: ({ children }: { children: React.ReactNode; variant?: string }) => (
<div role="alert">{children}</div>
),
AlertDescription: ({ children }: { children: React.ReactNode }) => (
<span>{children}</span>
),
LoadingButton: ({
children,
loading,
loadingText,
...rest
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
loading?: boolean;
loadingText?: string;
}) => (
<button {...rest} disabled={loading}>
{loading ? loadingText : children}
</button>
),
PasswordInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => (
<input type="password" {...props} />
),
}));

/* ---------- import under test ---------- */
import LoginPage from "../page";

/* ---------- helpers ---------- */

function mockFetchBootstrapStatus(registrationEnabled: boolean) {
global.fetch = vi.fn().mockResolvedValue({
json: () =>
Promise.resolve({
data: { bootstrapRequired: false, registrationEnabled },
}),
});
}

/* ---------- tests ---------- */

describe("LoginPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("shows the signup link when registration is enabled", async () => {
mockFetchBootstrapStatus(true);

render(<LoginPage />);

await waitFor(() => {
expect(screen.getByText("Sign up")).toBeDefined();
});

const signupLink = screen.getByText("Sign up");
expect(signupLink.closest("a")).toHaveAttribute("href", "/signup");
});

it("hides the signup link when registration is disabled", async () => {
mockFetchBootstrapStatus(false);

render(<LoginPage />);

await waitFor(() => {
expect(screen.queryByText("Sign up")).toBeNull();
});
});

it("shows the signup link by default before fetch completes", () => {
// Fetch never resolves — default state should show the link
global.fetch = vi.fn().mockReturnValue(new Promise(() => {}));

render(<LoginPage />);

expect(screen.getByText("Sign up")).toBeDefined();
});

it("keeps the signup link when fetch fails", async () => {
global.fetch = vi.fn().mockRejectedValue(new Error("Network error"));

render(<LoginPage />);

// Default state is registrationEnabled=true, fetch error doesn't change it
await waitFor(() => {
expect(global.fetch).toHaveBeenCalled();
});

expect(screen.getByText("Sign up")).toBeDefined();
});

it("renders the login form with email and password fields", () => {
mockFetchBootstrapStatus(true);

render(<LoginPage />);

expect(screen.getByLabelText("Email")).toBeDefined();
expect(screen.getByLabelText("Password")).toBeDefined();
expect(screen.getByText("Sign in")).toBeDefined();
});

it("renders the NeoBoard title", () => {
mockFetchBootstrapStatus(true);

render(<LoginPage />);

expect(screen.getByText("NeoBoard")).toBeDefined();
});

it("shows error message when login fails", async () => {
mockFetchBootstrapStatus(true);
mockSignIn.mockResolvedValue({ error: "CredentialsSignin" });

const user = userEvent.setup();
render(<LoginPage />);

const emailInput = screen.getByLabelText("Email");
const passwordInput = screen.getByLabelText("Password");
const submitButton = screen.getByText("Sign in");

await user.type(emailInput, "test@example.com");
await user.type(passwordInput, "wrongpassword");
await user.click(submitButton);

await waitFor(() => {
expect(screen.getByText("Invalid email or password")).toBeDefined();
});
});

it("redirects to callbackUrl on successful login", async () => {
mockFetchBootstrapStatus(true);
mockSignIn.mockResolvedValue({ error: null });

const user = userEvent.setup();
render(<LoginPage />);

const emailInput = screen.getByLabelText("Email");
const passwordInput = screen.getByLabelText("Password");
const submitButton = screen.getByText("Sign in");

await user.type(emailInput, "test@example.com");
await user.type(passwordInput, "correctpassword");
await user.click(submitButton);

await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith("/");
});
});
});
47 changes: 28 additions & 19 deletions app/src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { Suspense, useState } from "react";
import { Suspense, useState, useEffect } from "react";
import { signIn } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
Expand All @@ -16,10 +16,7 @@
Alert,
AlertDescription,
} from "@neoboard/components";
import {
LoadingButton,
PasswordInput,
} from "@neoboard/components";
import { LoadingButton, PasswordInput } from "@neoboard/components";

Check warning on line 19 in app/src/app/(auth)/login/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'@neoboard/components' imported multiple times.

See more on https://sonarcloud.io/project/issues?id=alfredo1996_neoboard&issues=AZ1U0kWo1_32ZbbIgMDg&open=AZ1U0kWo1_32ZbbIgMDg&pullRequest=349

function LoginForm() {
const router = useRouter();
Expand Down Expand Up @@ -69,12 +66,7 @@

<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<PasswordInput
id="password"
name="password"
required
minLength={6}
/>
<PasswordInput id="password" name="password" required minLength={6} />
</div>

<LoadingButton
Expand All @@ -90,26 +82,43 @@
}

export default function LoginPage() {
const [registrationEnabled, setRegistrationEnabled] = useState(true);

useEffect(() => {
fetch("/api/auth/bootstrap-status")
.then((r) => r.json())
.then((body) => {
const payload = body?.data ?? body;
setRegistrationEnabled(payload?.registrationEnabled !== false);
})
.catch(() => {});
}, []);

return (
<div className="flex min-h-screen items-center justify-center">
<Card className="w-full max-w-sm">
<CardHeader className="text-center">
<CardTitle className="text-2xl">NeoBoard</CardTitle>
<p className="text-sm text-muted-foreground">
Visual dashboards for Neo4j &amp; PostgreSQL
</p>
<CardDescription>Sign in to your account</CardDescription>
</CardHeader>
<CardContent>
<Suspense>
<LoginForm />
</Suspense>
</CardContent>
<CardFooter className="justify-center">
<p className="text-sm text-muted-foreground">
Don&apos;t have an account?{" "}
<Link href="/signup" className="text-primary underline">
Sign up
</Link>
</p>
</CardFooter>
{registrationEnabled && (
<CardFooter className="justify-center">
<p className="text-sm text-muted-foreground">
Don&apos;t have an account?{" "}
<Link href="/signup" className="text-primary underline">
Sign up
</Link>
</p>
</CardFooter>
)}
</Card>
</div>
);
Expand Down
Loading
Loading