Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import strawberry
import strawberry_django
from django.db.models import Case, IntegerField, Value, When

from apps.github.api.internal.nodes.repository import RepositoryNode
from apps.github.api.internal.nodes.user import UserNode
Expand Down Expand Up @@ -52,3 +53,23 @@ def user(

"""
return User.objects.filter(has_public_member_page=True, login=login).first()

@strawberry_django.field
def users(self, logins: list[str]) -> list[UserNode]:
"""Resolve users by their logins.

Args:
logins (list[str]): List of user logins.

Returns:
list[User]: List of user objects.

"""
qs = User.objects.filter(login__in=logins, has_public_member_page=True)
if logins:
order = Case(
*[When(login=login, then=Value(pos)) for pos, login in enumerate(logins)],
output_field=IntegerField(),
)
qs = qs.order_by(order)
return qs
26 changes: 13 additions & 13 deletions docker-compose/local/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ services:
- 8000:8000
volumes:
- ../../backend:/home/owasp
- backend-venv:/home/owasp/.venv
- backend-venv_3592:/home/owasp/.venv

cache:
command: >
Expand All @@ -50,7 +50,7 @@ services:
networks:
- nest-network
volumes:
- cache-data:/data
- cache-data_3592:/data

db:
container_name: nest-db
Expand All @@ -67,7 +67,7 @@ services:
networks:
- nest-network
volumes:
- db-data:/var/lib/postgresql/data
- db-data_3592:/var/lib/postgresql/data

docs:
container_name: nest-docs
Expand All @@ -84,7 +84,7 @@ services:
- 8001:8001
volumes:
- ../../docs:/home/owasp/docs
- docs-venv:/home/owasp/.venv
- docs-venv_3592:/home/owasp/.venv

frontend:
container_name: nest-frontend
Expand All @@ -106,8 +106,8 @@ services:
- 3000:3000
volumes:
- ../../frontend:/home/owasp
- frontend-next:/home/owasp/.next
- frontend-node-modules:/home/owasp/node_modules
- frontend-next_3592:/home/owasp/.next
- frontend-node-modules_3592:/home/owasp/node_modules

worker:
container_name: nest-worker
Expand Down Expand Up @@ -136,15 +136,15 @@ services:
- nest-network
volumes:
- ../../backend:/home/owasp
- backend-venv:/home/owasp/.venv
- backend-venv_3592:/home/owasp/.venv

networks:
nest-network:

volumes:
backend-venv:
cache-data:
db-data:
docs-venv:
frontend-next:
frontend-node-modules:
backend-venv_3592:
cache-data_3592:
db-data_3592:
docs-venv_3592:
frontend-next_3592:
frontend-node-modules_3592:
16 changes: 8 additions & 8 deletions frontend/__tests__/e2e/pages/About.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ test.describe('About Page', () => {
const request = route.request()
const postData = request.postDataJSON()

if (postData.query?.includes('user')) {
const username = postData.variables.key
const userData = mockAboutData.users[username]
await route.fulfill({ status: 200, json: { data: { user: userData } } })
} else if (postData.query?.includes('topContributors')) {
if (postData.operationName === 'GetAboutPageData') {
await route.fulfill({
status: 200,
json: { data: { topContributors: mockAboutData.topContributors } },
json: {
data: {
project: mockAboutData.project,
topContributors: mockAboutData.topContributors,
users: mockAboutData.users,
},
},
})
} else {
await route.fulfill({ status: 200, json: { data: { project: mockAboutData.project } } })
}
})

Expand Down
10 changes: 5 additions & 5 deletions frontend/__tests__/mockData/mockAboutData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,21 @@ export const mockAboutData = {
login: `contributor${i + 1}`,
name: `Contributor ${i + 1}`,
})),
users: {
arkid15r: {
users: [
{
avatarUrl: 'https://avatars.githubusercontent.com/u/2201626?v=4',
login: 'arkid15r',
name: 'Arkadii Yakovets',
},
kasya: {
{
avatarUrl: 'https://avatars.githubusercontent.com/u/5873153?v=4',
login: 'kasya',
name: 'Kate Golovanova',
},
mamicidal: {
{
avatarUrl: 'https://avatars.githubusercontent.com/u/112129498?v=4',
login: 'mamicidal',
name: 'Starr Brown',
},
},
],
}
150 changes: 22 additions & 128 deletions frontend/__tests__/unit/pages/About.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import { useRouter } from 'next/navigation'
import React, { act } from 'react'
import { render } from 'wrappers/testUtil'
import About from 'app/about/page'
import {
GetProjectMetadataDocument,
GetTopContributorsDocument,
} from 'types/__generated__/projectQueries.generated'
import { GetLeaderDataDocument } from 'types/__generated__/userQueries.generated'

jest.mock('@apollo/client/react', () => ({
...jest.requireActual('@apollo/client/react'),
useQuery: jest.fn(),
Expand Down Expand Up @@ -201,43 +195,13 @@ jest.mock('components/ShowMoreButton', () => ({
},
}))

const mockUserData = (username) => ({
data: { user: mockAboutData.users[username] },
loading: false,
error: null,
})

const mockProjectData = {
data: { project: mockAboutData.project },
loading: false,
error: null,
}

const mockTopContributorsData = {
data: { topContributors: mockAboutData.topContributors },
loading: false,
error: null,
}

describe('About Component', () => {
let mockRouter: { push: jest.Mock }
beforeEach(() => {
;(useQuery as unknown as jest.Mock).mockImplementation((query, options) => {
const key = options?.variables?.key

if (query === GetProjectMetadataDocument) {
if (key === 'nest') {
return mockProjectData
}
} else if (query === GetTopContributorsDocument) {
if (key === 'nest') {
return mockTopContributorsData
}
} else if (query === GetLeaderDataDocument) {
return mockUserData(key)
}

return { loading: true }
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: mockAboutData,
loading: false,
error: null,
})
mockRouter = { push: jest.fn() }
;(useRouter as jest.Mock).mockReturnValue(mockRouter)
Expand Down Expand Up @@ -451,13 +415,10 @@ describe('About Component', () => {
})

test('handles null project in data response gracefully', async () => {
;(useQuery as unknown as jest.Mock).mockImplementation((query, options) => {
if (options?.variables?.key === 'nest') {
return { data: { project: null }, loading: false, error: null }
} else if (['arkid15r', 'kasya', 'mamicidal'].includes(options?.variables?.key)) {
return mockUserData(options?.variables?.key)
}
return { loading: true }
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: { ...mockAboutData, project: null },
loading: false,
error: null,
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

await act(async () => {
Expand Down Expand Up @@ -488,30 +449,22 @@ describe('About Component', () => {

test('handles partial user data in leader response', async () => {
const partialUserData = {
avatarUrl: 'https://avatars.githubusercontent.com/u/2201626?v=4',
company: 'OWASP',
// name is missing
login: 'arkid15r',
url: '/members/arkid15r',
}

;(useQuery as unknown as jest.Mock).mockReturnValue({
data: {
user: {
avatarUrl: 'https://avatars.githubusercontent.com/u/2201626?v=4',
company: 'OWASP',
// name is missing
login: 'arkid15r',
url: '/members/arkid15r',
},
...mockAboutData,
users: mockAboutData.users.map((user) =>
user.login === 'arkid15r' ? partialUserData : user
),
},
loading: false,
error: null,
}

;(useQuery as unknown as jest.Mock).mockImplementation((query, options) => {
if (query === GetProjectMetadataDocument && options?.variables?.key === 'nest') {
return mockProjectData
} else if (query === GetTopContributorsDocument && options?.variables?.key === 'nest') {
return mockTopContributorsData
} else if (options?.variables?.key === 'arkid15r') {
return partialUserData
} else if (options?.variables?.key === 'kasya' || options?.variables?.key === 'mamicidal') {
return mockUserData(options?.variables?.key)
}
return { loading: true }
})

await act(async () => {
Expand Down Expand Up @@ -571,35 +524,9 @@ describe('About Component', () => {
})
})

test('triggers toaster error when GraphQL request fails for project', async () => {
test('triggers toaster error when GraphQL request fails', async () => {
;(useQuery as unknown as jest.Mock).mockImplementation((query, options) => {
if (query === GetProjectMetadataDocument && options?.variables?.key === 'nest') {
return { loading: false, data: null, error: new Error('GraphQL error') }
}
return {
loading: false,
data: { user: { avatarUrl: '', company: '', name: 'Dummy', location: '' } },
error: null,
}
})
await act(async () => {
render(<About />)
})
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith({
color: 'danger',
description: 'GraphQL error',
shouldShowTimeoutProgress: true,
timeout: 5000,
title: 'Server Error',
variant: 'solid',
})
})
})

test('triggers toaster error when GraphQL request fails for topContributors', async () => {
;(useQuery as unknown as jest.Mock).mockImplementation((query, options) => {
if (query === GetTopContributorsDocument && options?.variables?.key === 'nest') {
if (options?.variables?.key === 'nest') {
return { loading: false, data: null, error: new Error('GraphQL error') }
}
return {
Expand Down Expand Up @@ -684,37 +611,4 @@ describe('About Component', () => {
expect(screen.getByText('Timeline Event 2')).toBeInTheDocument()
expect(screen.queryByText('Timeline Event 1')).not.toBeInTheDocument()
})

test('triggers toaster error when GraphQL request fails for a leader', async () => {
;(useQuery as unknown as jest.Mock).mockImplementation((query, options) => {
if (query === GetLeaderDataDocument && options?.variables?.key === 'arkid15r') {
return { loading: false, data: null, error: new Error('GraphQL error for leader') }
}
if (query === GetProjectMetadataDocument) {
return mockProjectData
}
if (query === GetTopContributorsDocument) {
return mockTopContributorsData
}
if (query === GetLeaderDataDocument) {
return mockUserData(options?.variables?.key)
}
return { loading: true }
})

await act(async () => {
render(<About />)
})

await waitFor(() => {
expect(addToast).toHaveBeenCalledWith({
color: 'danger',
description: 'GraphQL error for leader',
shouldShowTimeoutProgress: true,
timeout: 5000,
title: 'Server Error',
variant: 'solid',
})
})
})
})
Loading