Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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')
})
})
4 changes: 2 additions & 2 deletions frontend/__tests__/unit/pages/CreateModule.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('CreateModulePage', () => {
})

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

;(useSession as jest.Mock).mockReturnValue({
data: { user: { login: 'admin-user' } },
Expand Down Expand Up @@ -95,7 +95,7 @@ describe('CreateModulePage', () => {
() => {
expect(mockQuery).toHaveBeenCalled()
},
{ timeout: 2000 }
{ 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
20 changes: 11 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 @@ -47,8 +47,8 @@ const providers = []
if (IS_GITHUB_AUTH_ENABLED) {
providers.push(
GitHubProvider({
clientId: process.env.NEXT_SERVER_GITHUB_CLIENT_ID,
clientSecret: process.env.NEXT_SERVER_GITHUB_CLIENT_SECRET,
clientId: GITHUB_CLIENT_ID!,
clientSecret: GITHUB_CLIENT_SECRET!,
profile(profile) {
Comment thread
anurag2787 marked this conversation as resolved.
return {
email: profile.email,
Expand Down Expand Up @@ -88,19 +88,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
29 changes: 16 additions & 13 deletions frontend/src/app/board/[year]/candidates/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,19 @@ type Candidate = {
memberEmail: string
description: string
member?: {
__typename?: string
avatarUrl: string
bio?: string
createdAt?: number
firstOwaspContributionAt?: number
bio: string
createdAt: number
firstOwaspContributionAt: number | null
id: string
isFormerOwaspStaff?: boolean
isGsocMentor?: boolean
isOwaspBoardMember?: boolean
linkedinPageId?: string
isFormerOwaspStaff: boolean
isGsocMentor: boolean
isOwaspBoardMember: boolean
linkedinPageId: string
login: string
name: string
}
} | null
}

type MemberSnapshot = {
Expand Down Expand Up @@ -332,11 +333,13 @@ const BoardCandidatesPage = () => {
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
window.open(
`https://linkedin.com/in/${candidate.member.linkedinPageId}`,
'_blank',
'noopener,noreferrer'
)
if (candidate.member?.linkedinPageId) {
window.open(
`https://linkedin.com/in/${candidate.member.linkedinPageId}`,
'_blank',
'noopener,noreferrer'
)
}
}}
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
aria-label={`${candidate.memberName}'s LinkedIn profile`}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/chapters/[chapterKey]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function generateMetadata({
keywords: ['owasp', 'security', 'chapter', chapterKey, chapter.name],
title: chapter.name,
})
: null
: {}
}

export default async function ChapterDetailsLayout({
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/app/chapters/[chapterKey]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useParams } from 'next/navigation'
import { useEffect } from 'react'
import { handleAppError, ErrorDisplay } from 'app/global-error'
import { GetChapterDataDocument } from 'types/__generated__/chapterQueries.generated'
import type { Chapter } from 'types/chapter'
import { getContributionStats } from 'utils/contributionDataUtils'
import { formatDate, getDateRange } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
Expand Down Expand Up @@ -55,9 +56,9 @@ export default function ChapterDetailsPage() {
}

const details = [
{ label: 'Last Updated', value: formatDate(chapter.updatedAt) },
{ label: 'Location', value: chapter.suggestedLocation },
{ label: 'Region', value: chapter.region },
{ label: 'Last Updated', value: formatDate(chapter.updatedAt) ?? '' },
{ label: 'Location', value: chapter.suggestedLocation ?? '' },
{ label: 'Region', value: chapter.region ?? '' },
{
label: 'URL',
value: (
Expand All @@ -83,7 +84,7 @@ export default function ChapterDetailsPage() {
endDate={endDate}
entityKey={chapter.key}
entityLeaders={chapter.entityLeaders}
geolocationData={[chapter]}
geolocationData={[chapter as unknown as Chapter]}
isActive={chapter.isActive}
socialLinks={chapter.relatedUrls}
startDate={startDate}
Expand Down
Loading