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
22 changes: 22 additions & 0 deletions backend/apps/github/api/internal/queries/organization.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"""GitHub organization GraphQL queries."""

import strawberry
import strawberry_django

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 +33,21 @@ def organization(
return Organization.objects.get(is_owasp_related_organization=True, login=login)
except Organization.DoesNotExist:
return None

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

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

Returns:
list: List of recent organizations.

"""
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()
})
})
79 changes: 79 additions & 0 deletions frontend/__tests__/e2e/data/mockCommunityData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
export const mockCommunityData = {
data: {
recentChapters: [
{
id: '1',
createdAt: '2025-03-18T01:03:09+00:00',
key: 'chapter_1',
leaders: ['Leader 1', 'Leader 3'],
name: 'Chapter 1',
suggestedLocation: 'Pune, Maharashtra, India',
},
{
id: '2',
createdAt: '2025-03-13T00:01:01+00:00',
key: 'chapter_2',
leaders: ['Leader 1', 'Leader 2'],
name: 'Chapter 2',
suggestedLocation: 'Location 2',
},
{
id: '3',
createdAt: '2025-02-25T02:04:57+00:00',
key: 'chapter_3',
leaders: ['Leader 1', 'Leader 2'],
name: 'Chapter 3',
suggestedLocation: 'Location 3',
},
],
recentOrganizations: [
{
id: '1',
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4',
login: 'org_1',
name: 'Organization 1',
},
{
id: '2',
avatarUrl: 'https://avatars.githubusercontent.com/u/2?v=4',
login: 'org_2',
name: 'Organization 2',
},
],
snapshots: [
{
id: '1',
key: 'snapshot_1',
title: 'Snapshot 1',
startAt: '2025-01-01',
endAt: '2025-01-31',
},
{
id: '2',
key: 'snapshot_2',
title: 'Snapshot 2',
startAt: '2025-02-01',
endAt: '2025-02-28',
},
],
topContributors: [
{
name: 'Contributor 1',
login: 'contributor_1',
avatarUrl: 'https://avatars.githubusercontent.com/u/3531020?v=4',
},
{
name: 'Contributor 2',
login: 'contributor_2',
avatarUrl: 'https://avatars.githubusercontent.com/u/862914?v=4',
},
],
statsOverview: {
activeChaptersStats: 150,
activeProjectsStats: 50,
contributorsStats: 5000,
countriesStats: 100,
slackWorkspaceStats: 35000,
},
},
}
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 }) => {
const navSection = page.locator('.grid.grid-cols-2').first()
await expect(navSection.getByRole('link', { name: 'Chapters' })).toBeVisible()
await expect(navSection.getByRole('link', { name: 'Members' })).toBeVisible()
await expect(navSection.getByRole('link', { name: 'Organizations' })).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'])
})
})
72 changes: 72 additions & 0 deletions frontend/__tests__/mockData/mockCommunityData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export const mockCommunityGraphQLData = {
recentChapters: [
{
id: '1',
createdAt: '2025-01-01T10:00:00Z',
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',
bio: 'Bio 1',
},
{
id: 'user2',
avatarUrl: 'https://example.com/user2.png',
login: 'user2',
name: 'User 2',
bio: 'Bio 2',
},
],
statsOverview: {
activeChaptersStats: 150,
activeProjectsStats: 50,
countriesStats: 100,
contributorsStats: 5000,
},
}
Loading