Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions frontend/__tests__/mockData/mockCommitteeDetailsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const mockCommitteeDetailsData = {
contributorsCount: 10,
forksCount: 5,
issuesCount: 3,
key: 'test_committee',
leaders: ['Leader 1', 'Leader 2'],
name: 'Test Committee',
relatedUrls: ['https://twitter.com/testcommittee', 'https://github.com/testcommittee'],
Expand Down
73 changes: 72 additions & 1 deletion frontend/__tests__/unit/components/ItemCardList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -537,10 +537,81 @@ describe('ItemCardList Component', () => {
/>
)

// When URL is missing, the title should render as plain text, not a link
const titleText = screen.getByText('Test Issue Title')
expect(titleText).toBeInTheDocument()

// Verify it's not wrapped in a link
const titleLinks = screen.queryAllByTestId('link')
const titleLink = titleLinks.find((link) => link.textContent?.includes('Test Issue Title'))
expect(titleLink).toBeUndefined()
})

it('renders title as link when valid url is present', () => {
const itemWithUrl = { ...mockIssue, url: 'https://example.com/issue' }

render(
<ItemCardList
title="With URL"
data={[itemWithUrl]}
renderDetails={defaultProps.renderDetails}
/>
)

const titleLinks = screen.getAllByTestId('link')
const titleLink = titleLinks.find((link) => link.textContent?.includes('Test Issue Title'))
expect(titleLink).toHaveAttribute('href', 'https://example.com/issue')
})

it('uses id as key when objectID is not present', () => {
const itemWithId = { ...mockIssue, objectID: undefined, id: 'test-id-123' }

render(
<ItemCardList
title="With ID"
data={[itemWithId]}
renderDetails={defaultProps.renderDetails}
/>
)

expect(screen.getByText('Test Issue Title')).toBeInTheDocument()
})

it('generates fallback key when no identifiers are present', () => {
const itemWithoutIds = {
...mockIssue,
objectID: undefined,
id: undefined,
repositoryName: '',
title: '',
name: '',
url: '',
}

render(
<ItemCardList
title="Fallback Key"
data={[itemWithoutIds]}
renderDetails={defaultProps.renderDetails}
/>
)

// Component should render without crashing
expect(screen.getByText('Fallback Key')).toBeInTheDocument()
})

it('uses objectID as primary key identifier', () => {
const itemWithObjectId = { ...mockIssue, objectID: 'object-123' }

render(
<ItemCardList
title="With ObjectID"
data={[itemWithObjectId]}
renderDetails={defaultProps.renderDetails}
/>
)

expect(titleLink).toHaveAttribute('href', '')
expect(screen.getByText('Test Issue Title')).toBeInTheDocument()
})
})

Expand Down
32 changes: 32 additions & 0 deletions frontend/__tests__/unit/components/SortBy.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,36 @@ describe('<SortBy />', () => {
expect(container).toBeInTheDocument()
expect(hiddenSelect).toHaveAccessibleName(/Sort By/)
})

it('toggles order when Enter key is pressed on sort order button', async () => {
await act(async () => {
render(<SortBy {...defaultProps} selectedOrder="asc" />)
})
await act(async () => {
const buttons = screen.getAllByRole('button')
const orderButton = buttons[1]
fireEvent.keyDown(orderButton, { key: 'Enter' })
})
expect(defaultProps.onOrderChange).toHaveBeenCalledWith('desc')
})

it('toggles order when Space key is pressed on sort order button', async () => {
await act(async () => {
render(<SortBy {...defaultProps} selectedOrder="desc" />)
})
await act(async () => {
const buttons = screen.getAllByRole('button')
const orderButton = buttons[1]
fireEvent.keyDown(orderButton, { key: ' ' })
})
expect(defaultProps.onOrderChange).toHaveBeenCalledWith('asc')
})

it('does not render order button when selectedSortOption is default', async () => {
await act(async () => {
render(<SortBy {...defaultProps} selectedSortOption="default" />)
})
const sortOrderButton = screen.queryByLabelText(/Sort in/i)
expect(sortOrderButton).not.toBeInTheDocument()
})
})
68 changes: 68 additions & 0 deletions frontend/__tests__/unit/components/ToggleableList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,72 @@ describe('ToggleableList', () => {
'hover:scale-105'
)
})

it('handles Enter key press on item button', () => {
render(<ToggleableList entityKey="test" items={['React', 'Vue', 'Angular']} label="Tags" />)
const button = screen.getByText('React')
fireEvent.keyDown(button, { key: 'Enter' })
expect(mockPush).toHaveBeenCalledWith('/projects?q=React')
})

it('handles Space key press on item button', () => {
render(<ToggleableList entityKey="test" items={['React', 'Vue', 'Angular']} label="Tags" />)
const button = screen.getByText('Vue')
fireEvent.keyDown(button, { key: ' ' })
expect(mockPush).toHaveBeenCalledWith('/projects?q=Vue')
})

it('prevents default behavior on keyboard event', () => {
render(<ToggleableList entityKey="test" items={['React']} label="Tags" />)
const button = screen.getByText('React')
fireEvent.keyDown(button, { key: 'Enter' })
expect(mockPush).toHaveBeenCalled()
})

it('does not navigate when button is disabled and clicked', () => {
render(
<ToggleableList entityKey="test" items={['React', 'Vue']} label="Tags" isDisabled={true} />
)
const button = screen.getByText('React')
fireEvent.click(button)
expect(mockPush).not.toHaveBeenCalled()
})

it('does not navigate when button is disabled and Enter key is pressed', () => {
render(<ToggleableList entityKey="test" items={['React']} label="Tags" isDisabled={true} />)
const button = screen.getByText('React')
fireEvent.keyDown(button, { key: 'Enter' })
expect(mockPush).not.toHaveBeenCalled()
})

it('does not navigate when button is disabled and Space key is pressed', () => {
render(<ToggleableList entityKey="test" items={['Vue']} label="Tags" isDisabled={true} />)
const button = screen.getByText('Vue')
fireEvent.keyDown(button, { key: ' ' })
expect(mockPush).not.toHaveBeenCalled()
})

it('has disabled attribute when isDisabled is true', () => {
render(<ToggleableList entityKey="test" items={['React']} label="Tags" isDisabled={true} />)
const button = screen.getByText('React')
expect(button).toBeDisabled()
})

it('does not have disabled attribute when isDisabled is false', () => {
render(<ToggleableList entityKey="test" items={['React']} label="Tags" isDisabled={false} />)
const button = screen.getByText('React')
expect(button).not.toBeDisabled()
})

it('applies cursor-default class when isDisabled is true', () => {
render(<ToggleableList entityKey="test" items={['React']} label="Tags" isDisabled={true} />)
const button = screen.getByText('React')
expect(button).toHaveClass('cursor-default')
})

it('applies cursor-pointer class when isDisabled is false', () => {
render(<ToggleableList entityKey="test" items={['React']} label="Tags" isDisabled={false} />)
const button = screen.getByText('React')
expect(button).toHaveClass('cursor-pointer')
})
})
88 changes: 45 additions & 43 deletions frontend/__tests__/unit/pages/CreateModule.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,54 +49,56 @@ describe('CreateModulePage', () => {
jest.clearAllMocks()
})

it('submits the form and navigates to programs page', async () => {
const user = userEvent.setup()

;(useSession as jest.Mock).mockReturnValue({
data: { user: { login: 'admin-user' } },
status: 'authenticated',
})
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: {
getProgram: {
admins: [{ login: 'admin-user' }],
},
},
loading: false,
})
;(useMutation as unknown as jest.Mock).mockReturnValue([
mockCreateModule.mockResolvedValue({
it(
'submits the form and navigates to programs page',
async () => {
const user = userEvent.setup({ delay: null })

;(useSession as jest.Mock).mockReturnValue({
data: { user: { login: 'admin-user' } },
status: 'authenticated',
})
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: {
createModule: {
key: 'my-test-module',
getProgram: {
admins: [{ login: 'admin-user' }],
},
},
}),
{ loading: false },
])

render(<CreateModulePage />)

// Fill all inputs
await user.type(screen.getByLabelText('Name'), 'My Test Module')
await user.type(screen.getByLabelText(/Description/i), 'This is a test module')
await user.type(screen.getByLabelText(/Start Date/i), '2025-07-15')
await user.type(screen.getByLabelText(/End Date/i), '2025-08-15')
await user.type(screen.getByLabelText(/Domains/i), 'AI, ML')
await user.type(screen.getByLabelText(/Tags/i), 'react, graphql')

const projectInput = await waitFor(() => {
return screen.getByPlaceholderText('Start typing project name...')
})
loading: false,
})
;(useMutation as unknown as jest.Mock).mockReturnValue([
mockCreateModule.mockResolvedValue({
data: {
createModule: {
key: 'my-test-module',
},
},
}),
{ loading: false },
])

await user.type(projectInput, 'Aw')
render(<CreateModulePage />)

await waitFor(
() => {
expect(mockQuery).toHaveBeenCalled()
},
{ timeout: 2000 }
)
// Fill all inputs
await user.type(screen.getByLabelText('Name'), 'My Test Module')
await user.type(screen.getByLabelText(/Description/i), 'This is a test module')
await user.type(screen.getByLabelText(/Start Date/i), '2025-07-15')
await user.type(screen.getByLabelText(/End Date/i), '2025-08-15')
await user.type(screen.getByLabelText(/Domains/i), 'AI, ML')
await user.type(screen.getByLabelText(/Tags/i), 'react, graphql')

const projectInput = await waitFor(() => {
return screen.getByPlaceholderText('Start typing project name...')
})

await user.type(projectInput, 'Aw')

await waitFor(
() => {
expect(mockQuery).toHaveBeenCalled()
},
{ timeout: 1000 }
)

const projectOption = await waitFor(
() => {
Expand Down
14 changes: 9 additions & 5 deletions frontend/src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { HiUserGroup } from 'react-icons/hi'
import { IconWrapper } from 'wrappers/IconWrapper'
import { ErrorDisplay, handleAppError } from 'app/global-error'
import { GetAboutPageDataDocument } from 'types/__generated__/aboutQueries.generated'
import type { Leader } from 'types/leader'
import {
technologies,
missionContent,
Expand Down Expand Up @@ -77,11 +78,14 @@ const About = () => {
const projectMetadata = data?.project
const topContributors = data?.topContributors

const leadersData = [data?.leader1, data?.leader2, data?.leader3].filter(Boolean).map((user) => ({
description: user?.login ? leaders[user.login as keyof typeof leaders] : '',
memberName: user?.name || user?.login,
member: user,
}))
const leadersData = [data?.leader1, data?.leader2, data?.leader3]
.filter(Boolean)
.map((user) => ({
description: user?.login ? leaders[user.login as keyof typeof leaders] : '',
memberName: user?.name || user?.login || '',
member: user,
}))
.filter((leader) => leader.memberName) as Leader[]

const [showAllRoadmap, setShowAllRoadmap] = useState(false)
const [showAllTimeline, setShowAllTimeline] = useState(false)
Expand Down
22 changes: 13 additions & 9 deletions frontend/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
IsProjectLeaderDocument,
} from 'types/__generated__/mentorshipQueries.generated'
import { ExtendedProfile, ExtendedSession } from 'types/auth'
import { IS_GITHUB_AUTH_ENABLED } from 'utils/env.server'
import { IS_GITHUB_AUTH_ENABLED, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } from 'utils/env.server'

async function checkIfProjectLeader(login: string): Promise<boolean> {
try {
Expand Down Expand Up @@ -45,10 +45,12 @@ async function checkIfMentor(login: string): Promise<boolean> {
const providers = []

if (IS_GITHUB_AUTH_ENABLED) {
const githubClientId = GITHUB_CLIENT_ID!
const githubClientSecret = GITHUB_CLIENT_SECRET!
providers.push(
GitHubProvider({
clientId: process.env.NEXT_SERVER_GITHUB_CLIENT_ID,
clientSecret: process.env.NEXT_SERVER_GITHUB_CLIENT_SECRET,
clientId: githubClientId,
clientSecret: githubClientSecret,
Comment thread
anurag2787 marked this conversation as resolved.
Outdated
profile(profile) {
Comment thread
anurag2787 marked this conversation as resolved.
return {
email: profile.email,
Expand Down Expand Up @@ -88,19 +90,21 @@ const authOptions: AuthOptions = {
}

if (trigger === 'update' && session) {
token.isOwaspStaff = (session as ExtendedSession).user.isOwaspStaff || false
const extSession = session as ExtendedSession
token.isOwaspStaff = extSession.user?.isOwaspStaff || false
}
Comment thread
anurag2787 marked this conversation as resolved.
return token
},

async session({ session, token }) {
;(session as ExtendedSession).accessToken = token.accessToken as string

if (session.user) {
;(session as ExtendedSession).user.login = token.login as string
;(session as ExtendedSession).user.isMentor = token.isMentor as boolean
;(session as ExtendedSession).user.isLeader = token.isLeader as boolean
;(session as ExtendedSession).user.isOwaspStaff = token.isOwaspStaff as boolean
if (session?.user) {
const extSession = session as ExtendedSession
extSession.user!.login = token.login as string
extSession.user!.isMentor = token.isMentor as boolean
extSession.user!.isLeader = token.isLeader as boolean
extSession.user!.isOwaspStaff = token.isOwaspStaff as boolean
}
return session
},
Expand Down
Loading