Skip to content

Commit 91334b4

Browse files
authored
Merge branch 'main' into feature/github-pull-request-tracking
2 parents 17297d6 + dae2361 commit 91334b4

File tree

8 files changed

+126
-4
lines changed

8 files changed

+126
-4
lines changed

backend/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ DJANGO_DB_PASSWORD=None
1111
DJANGO_DB_PORT=None
1212
DJANGO_DB_USER=None
1313
DJANGO_OPEN_AI_SECRET_KEY=None
14+
DJANGO_PUBLIC_IP_ADDRESS="127.0.0.1"
1415
DJANGO_RELEASE_VERSION=None
1516
DJANGO_SECRET_KEY=None
1617
DJANGO_SENTRY_DSN=None

backend/apps/common/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def get_nest_user_agent():
2222

2323
def get_user_ip_address(request):
2424
"""Return user's IP address."""
25+
if settings.ENVIRONMENT == "Local":
26+
return settings.PUBLIC_IP_ADDRESS
27+
2528
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
2629
return x_forwarded_for.split(",")[0] if x_forwarded_for else request.META.get("REMOTE_ADDR")
2730

backend/settings/local.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""OWASP Nest local configuration."""
22

3+
from configurations import values
4+
35
from settings.base import Base
46

57

@@ -13,5 +15,6 @@ class Local(Base):
1315
)
1416
DEBUG = True
1517
LOGGING = {}
18+
PUBLIC_IP_ADDRESS = values.Value()
1619
SLACK_COMMANDS_ENABLED = True
1720
SLACK_EVENTS_ENABLED = True

backend/tests/common/utils_test.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from datetime import datetime, timezone
2-
from unittest.mock import patch
2+
from unittest.mock import MagicMock, patch
33

44
import pytest
55
from django.conf import settings
66

77
from apps.common.utils import (
88
get_absolute_url,
9+
get_user_ip_address,
910
join_values,
1011
natural_date,
1112
natural_number,
@@ -57,3 +58,24 @@ def test_natural_date(self, mock_naturaltime, value, expected_calls):
5758
)
5859
def test_natural_number(self, value, unit, expected):
5960
assert natural_number(value, unit=unit) == expected
61+
62+
@pytest.mark.parametrize(
63+
("mock_request", "expected"),
64+
[
65+
({"HTTP_X_FORWARDED_FOR": "8.8.8.8"}, "8.8.8.8"),
66+
({"REMOTE_ADDR": "192.168.1.2"}, "192.168.1.2"),
67+
],
68+
)
69+
def test_get_user_ip_address(self, mock_request, expected):
70+
request = MagicMock()
71+
request.META = mock_request
72+
assert get_user_ip_address(request) == expected
73+
74+
def test_get_user_ip_address_local(self, mocker):
75+
request = MagicMock()
76+
request.META = {}
77+
78+
mocker.patch.object(settings, "ENVIRONMENT", "Local")
79+
mocker.patch.dict(settings._wrapped.__dict__, {"PUBLIC_IP_ADDRESS": "1.1.1.1"})
80+
81+
assert get_user_ip_address(request) == "1.1.1.1"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { render, fireEvent, waitFor } from '@testing-library/react'
2+
import ScrollToTop from 'components/ScrollToTop'
3+
4+
describe('ScrollToTop component test', () => {
5+
beforeEach(() => {
6+
window.scrollTo = jest.fn()
7+
Object.defineProperty(window, 'scrollY', { value: 0, writable: true })
8+
Object.defineProperty(window, 'innerHeight', { value: 1000, writable: true })
9+
})
10+
11+
afterEach(() => {
12+
jest.clearAllMocks()
13+
})
14+
15+
test('Initially, the button should be hidden', () => {
16+
const { getByLabelText } = render(<ScrollToTop />)
17+
const button = getByLabelText(/scroll to top/i)
18+
19+
expect(button).toHaveClass('opacity-0')
20+
expect(button).toHaveClass('pointer-events-none')
21+
})
22+
23+
test('The button should become visible after scrolling past the threshold', async () => {
24+
const { getByLabelText } = render(<ScrollToTop />)
25+
const button = getByLabelText(/scroll to top/i)
26+
27+
Object.defineProperty(window, 'scrollY', { value: 400, writable: true })
28+
window.dispatchEvent(new Event('scroll'))
29+
30+
await waitFor(() => {
31+
expect(button).toHaveClass('opacity-100')
32+
expect(button).toHaveClass('pointer-events-auto')
33+
})
34+
})
35+
36+
test('Clicking the button should call window.scrollTo with smooth behavior', async () => {
37+
const { getByLabelText } = render(<ScrollToTop />)
38+
const button = getByLabelText(/scroll to top/i)
39+
40+
Object.defineProperty(window, 'scrollY', { value: 400, writable: true })
41+
window.dispatchEvent(new Event('scroll'))
42+
43+
await waitFor(() => {
44+
expect(button).toHaveClass('opacity-100')
45+
})
46+
47+
fireEvent.click(button)
48+
expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' })
49+
})
50+
})

frontend/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ErrorDisplay, ERROR_CONFIGS } from 'wrappers/ErrorWrapper'
1818

1919
import Footer from 'components/Footer'
2020
import Header from 'components/Header'
21+
import ScrollToTop from 'components/ScrollToTop'
2122
import { Toaster } from 'components/ui/toaster'
2223

2324
function App() {
@@ -50,6 +51,7 @@ function App() {
5051
<Route path="*" element={<ErrorDisplay {...ERROR_CONFIGS['404']} />} />
5152
</Routes>
5253
<Footer />
54+
<ScrollToTop />
5355
</main>
5456
)
5557
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { faArrowUp } from '@fortawesome/free-solid-svg-icons'
2+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3+
import { useState, useEffect, useCallback } from 'react'
4+
5+
export default function ScrollToTop() {
6+
const [isVisible, setIsVisible] = useState(false)
7+
8+
const handleScroll = useCallback(() => {
9+
const scrollThreshold = window.innerHeight * 0.3
10+
setIsVisible(window.scrollY > scrollThreshold)
11+
}, [])
12+
13+
const scrollToTop = () => {
14+
window.scrollTo({ top: 0, behavior: 'smooth' })
15+
}
16+
17+
useEffect(() => {
18+
let scrollTimeout: ReturnType<typeof setTimeout> | null = null
19+
const throttledScroll = () => {
20+
if (!scrollTimeout) {
21+
scrollTimeout = setTimeout(() => {
22+
handleScroll()
23+
scrollTimeout = null
24+
}, 100)
25+
}
26+
}
27+
28+
window.addEventListener('scroll', throttledScroll)
29+
return () => window.removeEventListener('scroll', throttledScroll)
30+
}, [handleScroll])
31+
32+
return (
33+
<button
34+
onClick={scrollToTop}
35+
aria-label="Scroll to top"
36+
className={`duration-400 fixed bottom-4 right-4 z-50 flex h-11 w-11 items-center justify-center rounded-full bg-owasp-blue bg-opacity-70 text-white shadow-lg transition-all hover:scale-105 hover:bg-opacity-100 active:scale-100 dark:bg-opacity-30 dark:text-slate-300 hover:dark:bg-opacity-50 ${isVisible ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'} `}
37+
>
38+
<FontAwesomeIcon icon={faArrowUp} className="text-xl" />
39+
</button>
40+
)
41+
}

frontend/src/pages/Chapters.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ const ChaptersPage = () => {
3232
const searchParams = {
3333
indexName: 'chapters',
3434
query: '',
35-
currentPage: 1,
36-
hitsPerPage: 1000,
35+
currentPage,
36+
hitsPerPage: currentPage === 1 ? 1000 : 25,
3737
}
3838
const data: AlgoliaResponseType<ChapterTypeAlgolia> = await fetchAlgoliaData(
3939
searchParams.indexName,
@@ -44,7 +44,7 @@ const ChaptersPage = () => {
4444
setGeoLocData(data.hits)
4545
}
4646
fetchData()
47-
}, [])
47+
}, [currentPage])
4848

4949
const navigate = useNavigate()
5050
const renderChapterCard = (chapter: ChapterTypeAlgolia) => {

0 commit comments

Comments
 (0)