Skip to content
Closed
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
21 changes: 21 additions & 0 deletions backend/apps/github/api/internal/queries/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import strawberry

from apps.common.utils import normalize_limit
from apps.github.api.internal.nodes.organization import OrganizationNode
from apps.github.models.organization import Organization

MAX_LIMIT = 100


@strawberry.type
class OrganizationQuery:
Expand All @@ -29,3 +32,21 @@ def organization(
return Organization.objects.get(is_owasp_related_organization=True, login=login)
except Organization.DoesNotExist:
return None

@strawberry.field
def recent_organizations(self, limit: int = 5) -> list[OrganizationNode]:
"""Resolve recent organizations.

Args:
limit (int): The number of recent organizations to return.

Returns:
list: A list of recent organization.

"""
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

return Organization.objects.filter(is_owasp_related_organization=True).order_by(
"-created_at"
)[:normalized_limit]
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,35 @@ def test_organization_with_different_login(self, mock_get, mock_organization):

assert result == mock_organization
mock_get.assert_called_once_with(is_owasp_related_organization=True, login="test-org")

@patch("apps.github.models.organization.Organization.objects.filter")
def test_recent_organizations(self, mock_filter):
"""Test fetching recent organizations."""
mock_qs = Mock()
mock_ordered_qs = Mock()
mock_filter.return_value = mock_qs
mock_qs.order_by.return_value = mock_ordered_qs
mock_ordered_qs.__getitem__ = Mock(return_value=["org1", "org2"])

result = OrganizationQuery().recent_organizations(limit=2)

assert result == ["org1", "org2"]
mock_filter.assert_called_once_with(is_owasp_related_organization=True)
mock_qs.order_by.assert_called_once_with("-created_at")
mock_ordered_qs.__getitem__.assert_called_once_with(slice(None, 2))

@patch("apps.github.models.organization.Organization.objects.filter")
def test_recent_organizations_with_zero_limit(self, mock_filter):
"""Test fetching recent organizations with zero limit returns empty list."""
result = OrganizationQuery().recent_organizations(limit=0)

assert result == []
mock_filter.assert_not_called()

@patch("apps.github.models.organization.Organization.objects.filter")
def test_recent_organizations_with_negative_limit(self, mock_filter):
"""Test fetching recent organizations with negative limit returns empty list."""
result = OrganizationQuery().recent_organizations(limit=-1)

assert result == []
mock_filter.assert_not_called()
45 changes: 45 additions & 0 deletions frontend/__tests__/a11y/pages/Community.a11y.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useQuery } from '@apollo/client/react'
import { mockCommunityGraphQLData } from '@mockData/mockCommunityData'
import { screen, waitFor } from '@testing-library/react'
import { axe } from 'jest-axe'
import { render } from 'wrappers/testUtil'
import CommunityPage from 'app/community/page'

jest.mock('@apollo/client/react', () => ({
...jest.requireActual('@apollo/client/react'),
useQuery: jest.fn(),
}))

jest.mock('@heroui/toast', () => ({
addToast: jest.fn(),
}))

jest.mock('next/navigation', () => ({
...jest.requireActual('next/navigation'),
useRouter: jest.fn(() => ({
push: jest.fn(),
})),
}))

describe('Community Page Accessibility', () => {
afterAll(() => {
jest.clearAllMocks()
})

it('should have no accessibility violations', async () => {
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: mockCommunityGraphQLData,
loading: false,
error: null,
})

const { container } = render(<CommunityPage />)

await waitFor(() => {
expect(screen.getByText('OWASP Community')).toBeInTheDocument()
})

const results = await axe(container)
expect(results).toHaveNoViolations()
})
})
72 changes: 72 additions & 0 deletions frontend/__tests__/e2e/data/mockCommunityData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export const mockCommunityData = {
data: {
recentChapters: [
{
id: '1',
createdAt: '2025-01-01T10:00:00Z',
key: 'chapter-1',
leaders: ['Leader 1', 'Leader 2'],
name: 'Chapter 1',
suggestedLocation: 'Pune, Maharashtra, India',
},
{
id: '2',
createdAt: '2025-01-02T10:00:00Z',
key: 'chapter-2',
leaders: ['Leader 3'],
name: 'Chapter 2',
suggestedLocation: 'Location 2',
},
],
recentOrganizations: [
{
id: 'org1',
avatarUrl: 'https://example.com/org1.png',
login: 'org1',
name: 'Organization 1',
},
{
id: 'org2',
avatarUrl: 'https://example.com/org2.png',
login: 'org2',
name: 'Organization 2',
},
],
snapshots: [
{
id: 'snap1',
key: 'snapshot-1',
title: 'Snapshot 1',
startAt: '2025-01-01',
endAt: '2025-01-31',
},
{
id: 'snap2',
key: 'snapshot-2',
title: 'Snapshot 2',
startAt: '2025-02-01',
endAt: '2025-02-28',
},
],
topContributors: [
{
id: 'user1',
avatarUrl: 'https://example.com/user1.png',
login: 'user1',
name: 'Contributor 1',
},
{
id: 'user2',
avatarUrl: 'https://example.com/user2.png',
login: 'user2',
name: 'Contributor 2',
},
],
statsOverview: {
activeChaptersStats: 150,
activeProjectsStats: 50,
countriesStats: 100,
contributorsStats: 5000,
},
},
}
2 changes: 1 addition & 1 deletion frontend/__tests__/e2e/pages/Chapters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ test.describe('Chapters Page', () => {
})

test('breadcrumb renders correct segments on /chapters', async ({ page }) => {
await expectBreadCrumbsToBeVisible(page, ['Home', 'Chapters'])
await expectBreadCrumbsToBeVisible(page, ['Home', 'Community', 'Chapters'])
})
})
75 changes: 75 additions & 0 deletions frontend/__tests__/e2e/pages/Community.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { mockCommunityData } from '@e2e/data/mockCommunityData'
import { mockHomeData } from '@e2e/data/mockHomeData'
import { test, expect } from '@playwright/test'

test.describe('Community Page', () => {
test.beforeEach(async ({ page }) => {
await page.route('**/graphql/', async (route) => {
const combinedData = {
data: {
...mockHomeData.data,
...mockCommunityData.data,
},
}
await route.fulfill({
status: 200,
json: combinedData,
})
})
await page.context().addCookies([
{
name: 'csrftoken',
value: 'abc123',
domain: 'localhost',
path: '/',
},
])
await page.goto('/community')
})

test('should have a heading and intro text', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'OWASP Community' })).toBeVisible()
await expect(
page.getByText(
"Connect, collaborate, and contribute to the world's largest application security community."
)
).toBeVisible()
await expect(page.getByPlaceholder('Search the OWASP community')).toBeVisible()
})

test('should have navigation cards', async ({ page }) => {
await expect(page.getByRole('link', { name: /Chapters/ })).toBeVisible()
await expect(page.getByRole('link', { name: /Members/ })).toBeVisible()
await expect(page.getByRole('link', { name: /Organizations/ })).toBeVisible()
await expect(page.getByRole('link', { name: /Snapshots/ })).toBeVisible()
})

test('should have new chapters', async ({ page }) => {
await expect(page.getByText('New Chapters', { exact: true })).toBeVisible()
await expect(page.getByText('Chapter 1')).toBeVisible()
await expect(page.getByText('Pune, Maharashtra, India')).toBeVisible()
})

test('should have new organizations', async ({ page }) => {
await expect(page.getByText('New Organizations', { exact: true })).toBeVisible()
await expect(page.getByText('Organization 1')).toBeVisible()
})

test('should have snapshots', async ({ page }) => {
await expect(page.getByText('Snapshot 1')).toBeVisible()
await expect(page.getByText('Jan 1, 2025 - Jan 31, 2025')).toBeVisible()
})

test('should have top contributors', async ({ page }) => {
await expect(page.getByText('Top Contributors', { exact: true })).toBeVisible()
await expect(page.getByText('Contributor 1')).toBeVisible()
})

test('should have stats', async ({ page }) => {
await expect(page.getByText('Active Chapters')).toBeVisible()
await expect(page.getByText('150+', { exact: true })).toBeVisible()
await expect(page.getByText('Active Projects')).toBeVisible()
await expect(page.getByText('50+', { exact: true })).toBeVisible()
await expect(page.getByText(/5k\+/i)).toBeVisible()
})
})
2 changes: 1 addition & 1 deletion frontend/__tests__/e2e/pages/Organizations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ test.describe('Organization Page', () => {
})

test('breadcrumb renders correct segments on /organizations', async ({ page }) => {
await expectBreadCrumbsToBeVisible(page, ['Home', 'Organizations'])
await expectBreadCrumbsToBeVisible(page, ['Home', 'Community', 'Organizations'])
})
})
2 changes: 1 addition & 1 deletion frontend/__tests__/e2e/pages/Users.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ test.describe('Users Page', () => {
await expect(page.getByText('2k')).toBeVisible()
})
test('breadcrumb renders correct segments on /members', async ({ page }) => {
await expectBreadCrumbsToBeVisible(page, ['Home', 'Members'])
await expectBreadCrumbsToBeVisible(page, ['Home', 'Community', 'Members'])
})
})
70 changes: 70 additions & 0 deletions frontend/__tests__/mockData/mockCommunityData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
export const mockCommunityGraphQLData = {
recentChapters: [
{
id: '1',
createdAt: '2025-01-01T10:00:00Z',
Copy link
Contributor

Choose a reason for hiding this comment

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

P2: Mock recentChapters[].createdAt is an ISO date string, but the GraphQL schema and ChapterCard component expect a number (Unix timestamp). The mock should use a numeric timestamp to match the actual response shape. Consider also adding a type annotation (e.g., GetCommunityPageDataQuery) to prevent future mismatches.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/__tests__/mockData/mockCommunityData.ts, line 5:

<comment>Mock `recentChapters[].createdAt` is an ISO date string, but the GraphQL schema and `ChapterCard` component expect a `number` (Unix timestamp). The mock should use a numeric timestamp to match the actual response shape. Consider also adding a type annotation (e.g., `GetCommunityPageDataQuery`) to prevent future mismatches.</comment>

<file context>
@@ -0,0 +1,70 @@
+  recentChapters: [
+    {
+      id: '1',
+      createdAt: '2025-01-01T10:00:00Z',
+      key: 'chapter-1',
+      leaders: ['Leader 1', 'Leader 2'],
</file context>

key: 'chapter-1',
leaders: ['Leader 1', 'Leader 2'],
name: 'OWASP Chapter 1',
suggestedLocation: 'Location 1',
},
{
id: '2',
createdAt: '2025-01-02T10:00:00Z',
key: 'chapter-2',
leaders: ['Leader 3'],
name: 'OWASP Chapter 2',
suggestedLocation: 'Location 2',
},
],
recentOrganizations: [
{
id: 'org1',
avatarUrl: 'https://example.com/org1.png',
login: 'org1',
name: 'Organization 1',
},
{
id: 'org2',
avatarUrl: 'https://example.com/org2.png',
login: 'org2',
name: 'Organization 2',
},
],
snapshots: [
{
id: 'snap1',
key: 'snapshot-1',
title: 'Snapshot 1',
startAt: '2025-01-01',
endAt: '2025-01-31',
},
{
id: 'snap2',
key: 'snapshot-2',
title: 'Snapshot 2',
startAt: '2025-02-01',
endAt: '2025-02-28',
},
],
topContributors: [
{
id: 'user1',
avatarUrl: 'https://example.com/user1.png',
login: 'user1',
name: 'User 1',
},
{
id: 'user2',
avatarUrl: 'https://example.com/user2.png',
login: 'user2',
name: 'User 2',
},
],
statsOverview: {
activeChaptersStats: 150,
activeProjectsStats: 50,
countriesStats: 100,
contributorsStats: 5000,
},
}
Loading