Skip to content
Merged
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
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,7 @@ AUTH0_M2M_CLIENT_SECRET=your_m2m_client_secret
AUTH0_TENANT_DOMAIN=your-auth0-tenant-domain.auth0.com
```

> [!IMPORTANT]
> **Note**: MongoDB connection string should be in the format: `mongodb+srv://<username>:<password>@cluster.mongodb.net/<dbname>?retryWrites=true&w=majority`. Alternatively, if you are using a local MongoDB instance, it can be `mongodb://localhost:27017/collabify`.
> [!IMPORTANT] > **Note**: MongoDB connection string should be in the format: `mongodb+srv://<username>:<password>@cluster.mongodb.net/<dbname>?retryWrites=true&w=majority`. Alternatively, if you are using a local MongoDB instance, it can be `mongodb://localhost:27017/collabify`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The new single-line format for the [!IMPORTANT] admonition block is incorrect and will not render as intended on GitHub. To ensure it displays correctly, it should be restored to the multi-line blockquote format.

Suggested change
> [!IMPORTANT] > **Note**: MongoDB connection string should be in the format: `mongodb+srv://<username>:<password>@cluster.mongodb.net/<dbname>?retryWrites=true&w=majority`. Alternatively, if you are using a local MongoDB instance, it can be `mongodb://localhost:27017/collabify`.
> [!IMPORTANT]
> **Note**: MongoDB connection string should be in the format: `mongodb+srv://<username>:<password>@cluster.mongodb.net/<dbname>?retryWrites=true&w=majority`. Alternatively, if you are using a local MongoDB instance, it can be `mongodb://localhost:27017/collabify`.

Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The formatting change on this line appears to have broken the Markdown admonition block syntax. The original format had the admonition marker on a separate line from the content, which is the correct GitHub-flavored Markdown syntax:

> [!IMPORTANT]
> **Note**: ...content...

The new format combines them on one line with a > separator in the middle, which will likely not render correctly as an admonition block and may just appear as a regular blockquote.

Suggested change
> [!IMPORTANT] > **Note**: MongoDB connection string should be in the format: `mongodb+srv://<username>:<password>@cluster.mongodb.net/<dbname>?retryWrites=true&w=majority`. Alternatively, if you are using a local MongoDB instance, it can be `mongodb://localhost:27017/collabify`.
> [!IMPORTANT]
> **Note**: MongoDB connection string should be in the format: `mongodb+srv://<username>:<password>@cluster.mongodb.net/<dbname>?retryWrites=true&w=majority`. Alternatively, if you are using a local MongoDB instance, it can be `mongodb://localhost:27017/collabify`.

Copilot uses AI. Check for mistakes.

## Why Auth0?

Expand All @@ -275,8 +274,7 @@ Collabify uses Auth0 for secure authentication and authorization. Auth0 provides

With Auth0, Collabify can provide a secure and efficient authentication process, ensuring that user data is protected and that users have the appropriate access to the features they need.

> [!CAUTION]
> **Disclaimer:** I strongly believe that Auth0 is on its way to becoming the best authentication solution for web applications. It is a powerful tool that simplifies the authentication process and provides a wide range of features to enhance security and user experience. However, please note that this is my personal opinion, and I encourage you to evaluate different authentication solutions based on your specific needs and requirements - it does not have to be Auth0, and this project has been designed to make it easy to switch to another provider if you so choose!
> [!CAUTION] > **Disclaimer:** I strongly believe that Auth0 is on its way to becoming the best authentication solution for web applications. It is a powerful tool that simplifies the authentication process and provides a wide range of features to enhance security and user experience. However, please note that this is my personal opinion, and I encourage you to evaluate different authentication solutions based on your specific needs and requirements - it does not have to be Auth0, and this project has been designed to make it easy to switch to another provider if you so choose!
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Similar to the [!IMPORTANT] block, the [!CAUTION] admonition has been changed to a single-line format that will not render correctly. Please revert it to the proper multi-line blockquote format.

Suggested change
> [!CAUTION] > **Disclaimer:** I strongly believe that Auth0 is on its way to becoming the best authentication solution for web applications. It is a powerful tool that simplifies the authentication process and provides a wide range of features to enhance security and user experience. However, please note that this is my personal opinion, and I encourage you to evaluate different authentication solutions based on your specific needs and requirements - it does not have to be Auth0, and this project has been designed to make it easy to switch to another provider if you so choose!
> [!CAUTION]
> **Disclaimer:** I strongly believe that Auth0 is on its way to becoming the best authentication solution for web applications. It is a powerful tool that simplifies the authentication process and provides a wide range of features to enhance security and user experience. However, please note that this is my personal opinion, and I encourage you to evaluate different authentication solutions based on your specific needs and requirements - it does not have to be Auth0, and this project has been designed to make it easy to switch to another provider if you so choose!

Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Similar to line 254, this admonition block formatting has been changed in a way that will likely break the rendering. The original format with the [!CAUTION] marker on a separate line from the content is the correct GitHub-flavored Markdown syntax. Combining them on one line with a > separator will prevent it from rendering as a caution admonition block.

Suggested change
> [!CAUTION] > **Disclaimer:** I strongly believe that Auth0 is on its way to becoming the best authentication solution for web applications. It is a powerful tool that simplifies the authentication process and provides a wide range of features to enhance security and user experience. However, please note that this is my personal opinion, and I encourage you to evaluate different authentication solutions based on your specific needs and requirements - it does not have to be Auth0, and this project has been designed to make it easy to switch to another provider if you so choose!
> [!CAUTION]
> **Disclaimer:** I strongly believe that Auth0 is on its way to becoming the best authentication solution for web applications. It is a powerful tool that simplifies the authentication process and provides a wide range of features to enhance security and user experience. However, please note that this is my personal opinion, and I encourage you to evaluate different authentication solutions based on your specific needs and requirements - it does not have to be Auth0, and this project has been designed to make it easy to switch to another provider if you so choose!

Copilot uses AI. Check for mistakes.

## Auth0 Setup Guide

Expand Down Expand Up @@ -486,6 +484,7 @@ To run the Rails backend, follow these steps:
Collabify uses GitHub Actions for continuous integration and deployment (CI/CD). The workflow is defined in the `.github/workflows/workflow.yml` file. This workflow automatically builds and deploys the application to Vercel whenever changes are pushed to the main branch.

To set up GitHub Actions for your repository:

1. **Create or modify the `.github/workflows/workflow.yml` file** in your repository.
2. **Add the necessary secrets** in your GitHub repository settings (e.g., `VERCEL_TOKEN`, `VERCEL_PROJECT_ID`, etc.) to allow GitHub Actions to deploy to Vercel.
3. **Push your changes to the main branch.** The workflow will automatically trigger and deploy your application.
Expand Down
60 changes: 44 additions & 16 deletions __tests__/AdminPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,57 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import AdminPageInternal from '@/pages/admin';
import * as auth0 from '@auth0/nextjs-auth0/client';
import React from "react";
import { render, screen } from "@testing-library/react";
import AdminPageInternal from "@/pages/admin";
import * as auth0 from "@auth0/nextjs-auth0/client";

describe('AdminPageInternal', () => {
describe("AdminPageInternal", () => {
afterEach(() => jest.restoreAllMocks());

it('shows loader while loading roles and user', () => {
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: null, error: null, isLoading: true });
it("shows loader while loading roles and user", () => {
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: null, error: null, isLoading: true });
render(<AdminPageInternal />);
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
expect(screen.getByRole("img", { hidden: true })).toBeInTheDocument();
});

it('denies access if not admin', async () => {
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: { sub: 'foo' }, error: null, isLoading: false });
global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ roles: ['user'] }) });
it("denies access if not admin", async () => {
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: { sub: "foo" }, error: null, isLoading: false });
global.fetch = jest
.fn()
.mockResolvedValue({ ok: true, json: async () => ({ roles: ["user"] }) });
render(<AdminPageInternal />);
expect(await screen.findByText(/no permission/i)).toBeInTheDocument();
});

it('renders logs charts for admin', async () => {
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: { sub: 'admin|1' }, error: null, isLoading: false });
global.fetch = jest.fn()
.mockResolvedValueOnce({ ok: true, json: async () => ({ roles: ['admin'] }) })
.mockResolvedValueOnce({ ok: true, json: async () => ({ logs: [{ _id: '1', type: 'login', ip: '127.0.0.1', date: new Date().toISOString() }] }) });
it("renders logs charts for admin", async () => {
jest
.spyOn(auth0, "useUser")
.mockReturnValue({
user: { sub: "admin|1" },
error: null,
isLoading: false,
});
global.fetch = jest
.fn()
.mockResolvedValueOnce({
ok: true,
json: async () => ({ roles: ["admin"] }),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
logs: [
{
_id: "1",
type: "login",
ip: "127.0.0.1",
date: new Date().toISOString(),
},
],
}),
});
render(<AdminPageInternal />);
expect(await screen.findByText(/admin panel/i)).toBeInTheDocument();
});
Expand Down
48 changes: 34 additions & 14 deletions __tests__/DashboardPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,47 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import DashboardPageInternal from '@/pages/dashboard';
import * as auth0 from '@auth0/nextjs-auth0/client';
import React from "react";
import { render, screen } from "@testing-library/react";
import DashboardPageInternal from "@/pages/dashboard";
import * as auth0 from "@auth0/nextjs-auth0/client";

describe('DashboardPageInternal', () => {
describe("DashboardPageInternal", () => {
afterEach(() => jest.restoreAllMocks());

it('shows spinner on load', () => {
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: null, error: null, isLoading: true });
it("shows spinner on load", () => {
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: null, error: null, isLoading: true });
render(<DashboardPageInternal />);
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
expect(screen.getByRole("img", { hidden: true })).toBeInTheDocument();
});

it('prompts login when no session', async () => {
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: null, error: null, isLoading: false });
it("prompts login when no session", async () => {
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: null, error: null, isLoading: false });
render(<DashboardPageInternal />);
expect(await screen.findByText(/please log in/i)).toBeInTheDocument();
});

it('renders stats when session exists', async () => {
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: { sub: 'u1' }, error: null, isLoading: false });
const mockData = { userSub: 'u1', totalProjects: 2, totalTasks: 5, doneTasks: 3, todoTasks: 1, inProgressTasks: 1, topProjects: [], largestProjectName: '', smallestProjectName: '', projectStats: [], allProjects: [] };
global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => mockData });
it("renders stats when session exists", async () => {
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: { sub: "u1" }, error: null, isLoading: false });
const mockData = {
userSub: "u1",
totalProjects: 2,
totalTasks: 5,
doneTasks: 3,
todoTasks: 1,
inProgressTasks: 1,
topProjects: [],
largestProjectName: "",
smallestProjectName: "",
projectStats: [],
allProjects: [],
};
global.fetch = jest
.fn()
.mockResolvedValue({ ok: true, json: async () => mockData });
render(<DashboardPageInternal />);
expect(await screen.findByText(/2/)).toBeInTheDocument();
expect(screen.getByText(/5/)).toBeInTheDocument();
Expand Down
48 changes: 31 additions & 17 deletions __tests__/ProfilePage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,52 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import ProfilePageInternal from '@/pages/profile';
import * as auth0 from '@auth0/nextjs-auth0/client';
import React from "react";
import { render, screen } from "@testing-library/react";
import ProfilePageInternal from "@/pages/profile";
import * as auth0 from "@auth0/nextjs-auth0/client";

describe('ProfilePageInternal', () => {
describe("ProfilePageInternal", () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('shows loading spinner when isLoading is true', () => {
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: null, error: null, isLoading: true });
it("shows loading spinner when isLoading is true", () => {
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: null, error: null, isLoading: true });
render(<ProfilePageInternal />);
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
expect(screen.getByRole("img", { hidden: true })).toBeInTheDocument();
});

it('shows error message when error is returned', () => {
const err = new Error('Test error');
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: null, error: err, isLoading: false });
it("shows error message when error is returned", () => {
const err = new Error("Test error");
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: null, error: err, isLoading: false });
render(<ProfilePageInternal />);
expect(screen.getByText(/Test error/)).toBeInTheDocument();
});

it('prompts login when not authenticated', () => {
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: null, error: null, isLoading: false });
it("prompts login when not authenticated", () => {
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: null, error: null, isLoading: false });
render(<ProfilePageInternal />);
expect(screen.getByText(/Please log in/i)).toBeInTheDocument();
});

it('renders profile card when user is present', async () => {
const fakeUser = { name: 'Alice', email: 'a@b.com', picture: '', updated_at: new Date().toISOString(), email_verified: true };
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: fakeUser, error: null, isLoading: false });
it("renders profile card when user is present", async () => {
const fakeUser = {
name: "Alice",
email: "a@b.com",
picture: "",
updated_at: new Date().toISOString(),
email_verified: true,
};
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: fakeUser, error: null, isLoading: false });
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({ user: { name: 'Alice', nickname: 'Al' } }),
json: async () => ({ user: { name: "Alice", nickname: "Al" } }),
});
render(<ProfilePageInternal />);
expect(await screen.findByText(/Alice/)).toBeInTheDocument();
Expand Down
56 changes: 37 additions & 19 deletions __tests__/ProjectDetailPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,54 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import ProjectDetailPageInternal from '@/pages/projects/[id]';
import * as auth0 from '@auth0/nextjs-auth0/client';
import { useRouter } from 'next/router';
import React from "react";
import { render, screen } from "@testing-library/react";
import ProjectDetailPageInternal from "@/pages/projects/[id]";
import * as auth0 from "@auth0/nextjs-auth0/client";
import { useRouter } from "next/router";

jest.mock('next/router', () => ({ useRouter: jest.fn() }));
jest.mock("next/router", () => ({ useRouter: jest.fn() }));

describe('ProjectDetailPageInternal', () => {
describe("ProjectDetailPageInternal", () => {
beforeEach(() => {
(useRouter as jest.Mock).mockReturnValue({ query: { id: 'abc' } });
(useRouter as jest.Mock).mockReturnValue({ query: { id: "abc" } });
});
afterEach(() => jest.restoreAllMocks());

it('shows loader when loading project', () => {
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: null, isLoading: true });
it("shows loader when loading project", () => {
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: null, isLoading: true });
render(<ProjectDetailPageInternal />);
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
expect(screen.getByRole("img", { hidden: true })).toBeInTheDocument();
});

it('shows error message when fetch fails', async () => {
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: { sub: 'u1' }, isLoading: false });
it("shows error message when fetch fails", async () => {
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: { sub: "u1" }, isLoading: false });
global.fetch = jest.fn().mockResolvedValue({ ok: false });
render(<ProjectDetailPageInternal />);
expect(await screen.findByText(/failed to fetch project/i)).toBeInTheDocument();
expect(
await screen.findByText(/failed to fetch project/i),
).toBeInTheDocument();
});

it('renders project details when fetched', async () => {
const project = { projectId: 'abc', name: 'Test', description: 'Desc', tasks: [], membership: [] };
jest.spyOn(auth0, 'useUser').mockReturnValue({ user: { sub: 'u1' }, isLoading: false });
global.fetch = jest.fn()
it("renders project details when fetched", async () => {
const project = {
projectId: "abc",
name: "Test",
description: "Desc",
tasks: [],
membership: [],
};
jest
.spyOn(auth0, "useUser")
.mockReturnValue({ user: { sub: "u1" }, isLoading: false });
global.fetch = jest
.fn()
.mockResolvedValueOnce({ ok: true, json: async () => ({ project }) })
.mockResolvedValueOnce({ ok: true, json: async () => ({ membership: [] }) })
.mockResolvedValueOnce({
ok: true,
json: async () => ({ membership: [] }),
})
.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
render(<ProjectDetailPageInternal />);
expect(await screen.findByText(/Test/)).toBeInTheDocument();
Expand Down
28 changes: 16 additions & 12 deletions __tests__/ProjectsPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import ProjectsPageInternal from '@/pages/projects';
import * as auth0 from '@auth0/nextjs-auth0/client';
import React from "react";
import { render, screen } from "@testing-library/react";
import ProjectsPageInternal from "@/pages/projects";
import * as auth0 from "@auth0/nextjs-auth0/client";

describe('ProjectsPageInternal', () => {
describe("ProjectsPageInternal", () => {
afterEach(() => jest.restoreAllMocks());

it('renders loading spinner initially', () => {
it("renders loading spinner initially", () => {
render(<ProjectsPageInternal />);
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
expect(screen.getByRole("img", { hidden: true })).toBeInTheDocument();
});

it('shows empty state when no projects', async () => {
global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ projects: [] }) });
it("shows empty state when no projects", async () => {
global.fetch = jest
.fn()
.mockResolvedValue({ ok: true, json: async () => ({ projects: [] }) });
render(<ProjectsPageInternal />);
expect(await screen.findByText(/no projects yet/i)).toBeInTheDocument();
});

it('lists projects when fetched', async () => {
const projects = [{ projectId: 'abc', name: 'X', description: 'Y' }];
global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ projects }) });
it("lists projects when fetched", async () => {
const projects = [{ projectId: "abc", name: "X", description: "Y" }];
global.fetch = jest
.fn()
.mockResolvedValue({ ok: true, json: async () => ({ projects }) });
render(<ProjectsPageInternal />);
expect(await screen.findByText(/X/)).toBeInTheDocument();
});
Expand Down
24 changes: 13 additions & 11 deletions __tests__/api/dashboard.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
const { createMocks } = require('node-mocks-http')
const handler = require('../../pages/api/dashboard').default
jest.mock('@auth0/nextjs-auth0', () => ({ getSession: jest.fn() }))
jest.mock('../../lib/mongodb', () => ({ dbConnect: jest.fn() }))
jest.mock('../../models/Project', () => ({ find: jest.fn().mockReturnValue([]) }))
const { createMocks } = require("node-mocks-http");
const handler = require("../../pages/api/dashboard").default;
jest.mock("@auth0/nextjs-auth0", () => ({ getSession: jest.fn() }));
jest.mock("../../lib/mongodb", () => ({ dbConnect: jest.fn() }));
jest.mock("../../models/Project", () => ({
find: jest.fn().mockReturnValue([]),
}));

it('returns 401 if unauthenticated', async () => {
require('@auth0/nextjs-auth0').getSession.mockResolvedValue(null)
const { req, res } = createMocks()
await handler(req, res)
expect(res._getStatusCode()).toBe(401)
})
it("returns 401 if unauthenticated", async () => {
require("@auth0/nextjs-auth0").getSession.mockResolvedValue(null);
const { req, res } = createMocks();
await handler(req, res);
expect(res._getStatusCode()).toBe(401);
});
18 changes: 9 additions & 9 deletions __tests__/api/openapi.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const { createMocks } = require('node-mocks-http')
const handler = require('../../pages/api/openapi').default
jest.mock('../../utils/openapiSpec', () => ({ ok: true }))
const { createMocks } = require("node-mocks-http");
const handler = require("../../pages/api/openapi").default;
jest.mock("../../utils/openapiSpec", () => ({ ok: true }));

test('openapi returns spec', async () => {
const { req, res } = createMocks()
await handler(req, res)
expect(res._getStatusCode()).toBe(200)
expect(res._getJSONData()).toEqual({ ok: true })
})
test("openapi returns spec", async () => {
const { req, res } = createMocks();
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(res._getJSONData()).toEqual({ ok: true });
});
Loading
Loading