Skip to content
Merged
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
53 changes: 53 additions & 0 deletions tests/unit_tests/test_tethys_portal/test_views/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ def test_get_csrf_not_authenticated(self):
response = self.client.get(reverse("api:get_csrf"))
self.assertEqual(response.status_code, 401)

@override_settings(ENABLE_OPEN_PORTAL=True)
def test_get_csrf_not_authenticated_but_open_portal(self):
"""Test get_csrf API endpoint not authenticated."""
self.client.force_login(self.user)
response = self.client.get(reverse("api:get_csrf"))
self.assertEqual(response.status_code, 200)
self.assertIsInstance(response, HttpResponse)
self.assertIn("X-CSRFToken", response.headers)

def test_get_csrf_authenticated(self):
"""Test get_csrf API endpoint authenticated."""
self.client.force_login(self.user)
Expand All @@ -48,6 +57,17 @@ def test_get_session_not_authenticated(self):
response = self.client.get(reverse("api:get_session"))
self.assertEqual(response.status_code, 401)

@override_settings(ENABLE_OPEN_PORTAL=True)
def test_get_session_not_authenticated_but_open_portal(self):
"""Test get_session API endpoint not authenticated."""
response = self.client.get(reverse("api:get_session"))
self.assertEqual(response.status_code, 200)
self.assertIsInstance(response, JsonResponse)
# self.assertIn('Set-Cookie', response.headers)
json = response.json()
self.assertIn("isAuthenticated", json)
self.assertTrue(json["isAuthenticated"])

def test_get_session_authenticated(self):
"""Test get_session API endpoint authenticated."""
self.client.force_login(self.user)
Expand All @@ -64,6 +84,15 @@ def test_get_whoami_not_authenticated(self):
response = self.client.get(reverse("api:get_whoami"))
self.assertEqual(response.status_code, 401)

@override_settings(ENABLE_OPEN_PORTAL=True)
def test_get_whoami_not_authenticated_but_open_portal(self):
"""Test get_whoami API endpoint not authenticated."""
response = self.client.get(reverse("api:get_whoami"))
self.assertEqual(response.status_code, 200)
self.assertIsInstance(response, JsonResponse)
json = response.json()
self.assertDictEqual({}, json)

def test_get_whoami_authenticated(self):
"""Test get_whoami API endpoint authenticated."""
self.client.force_login(self.user)
Expand All @@ -77,9 +106,33 @@ def test_get_whoami_authenticated(self):
self.assertIn("email", json)
self.assertIn("isAuthenticated", json)
self.assertIn("isStaff", json)
self.assertIn("gravatarUrl", json)
self.assertEqual("foo", json["username"])
self.assertTrue(json["isAuthenticated"])

def test_get_whoami_authenticated_gravatar_exception(self):
"""Test get_whoami API endpoint when gravatar fails."""
from unittest.mock import patch

self.client.force_login(self.user)
with patch(
"tethys_portal.views.api.get_gravatar_url",
side_effect=Exception("Gravatar error"),
):
response = self.client.get(reverse("api:get_whoami"))
self.assertEqual(response.status_code, 200)
self.assertIsInstance(response, JsonResponse)
json = response.json()
self.assertIn("username", json)
self.assertIn("firstName", json)
self.assertIn("lastName", json)
self.assertIn("email", json)
self.assertIn("isAuthenticated", json)
self.assertIn("isStaff", json)
self.assertNotIn("gravatarUrl", json)
self.assertEqual("foo", json["username"])
self.assertTrue(json["isAuthenticated"])

@override_settings(MULTIPLE_APP_MODE=True)
@override_settings(STATIC_URL="/static")
@override_settings(PREFIX_URL="/")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
"author": "",
"license": "ISC",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@restart/hooks": "^0.5.0",
"@restart/ui": "^1.9.4",
"axios": "^0.27.2",
"bootstrap": "^5.1.3",
"color": "^4.2.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,54 @@
import styled from 'styled-components';
import Button from 'react-bootstrap/Button';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import PropTypes from 'prop-types';
import Tooltip from 'react-bootstrap/Tooltip';

import styled from "styled-components";
import Button from "react-bootstrap/Button";
import OverlayTrigger from "react-bootstrap/OverlayTrigger";
import PropTypes from "prop-types";
import Tooltip from "react-bootstrap/Tooltip";

const StyledButton = styled(Button)`
background-color: rgba(255, 255, 255, 0.1);
border: none;
color: white;

&:hover, &:focus {
background-color: rgba(0, 0, 0, 0.1)!important;
&:hover,
&:focus {
background-color: rgba(0, 0, 0, 0.1) !important;
color: white;
border: none;
box-shadow: none;
}
`;


const HeaderButton = ({children, tooltipPlacement, tooltipText, href, ...props}) => {
const HeaderButton = ({
children,
tooltipPlacement,
tooltipText,
href,
...props
}) => {
const styledButton = (
<StyledButton href={href} variant="light" size="sm" {...props}>{children}</StyledButton>
<StyledButton href={href} variant="outline-light" size="sm" {...props}>
{children}
</StyledButton>
);
const styledButtonWithTooltip = (
<OverlayTrigger
key={tooltipPlacement}
placement={tooltipPlacement}
overlay={
<Tooltip id={`tooltip-${tooltipPlacement}`}>
{tooltipText}
</Tooltip>
<Tooltip id={`tooltip-${tooltipPlacement}`}>{tooltipText}</Tooltip>
}
>
{styledButton}
</OverlayTrigger>
);
return tooltipText ? styledButtonWithTooltip : styledButton;
}
};

HeaderButton.propTypes = {
children: PropTypes.element,
tooltipPlacement: PropTypes.oneOf(['top', 'bottom', 'left', 'right']),
tooltipPlacement: PropTypes.oneOf(["top", "bottom", "left", "right"]),
tooltipText: PropTypes.string,
href: PropTypes.string,
};

export default HeaderButton
export default HeaderButton;
Original file line number Diff line number Diff line change
@@ -1,48 +1,91 @@
import styled from 'styled-components';
import Container from 'react-bootstrap/Container';
import Form from 'react-bootstrap/Form';
import Navbar from 'react-bootstrap/Navbar';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { BsX, BsGear } from 'react-icons/bs';
import { LinkContainer } from 'react-router-bootstrap';
import styled from "styled-components";
import Container from "react-bootstrap/Container";
import Form from "react-bootstrap/Form";
import Navbar from "react-bootstrap/Navbar";
import PropTypes from "prop-types";
import { useContext } from "react";
import { BsX, BsGear } from "react-icons/bs";
import { LinkContainer } from "react-router-bootstrap";

import HeaderButton from 'components/buttons/HeaderButton';
import NavButton from 'components/buttons/NavButton';
import { AppContext } from 'components/context';
import HeaderButton from "components/buttons/HeaderButton";
import NavButton from "components/buttons/NavButton";
import UserHeaderMenu from "components/layout/UserHeaderMenu";
import { AppContext } from "components/context";
import { getTethysPortalBase } from "services/utilities";

const CustomNavBar = styled(Navbar)`
min-height: var(--ts-header-height);
`;

const Header = ({onNavChange}) => {
const {tethysApp, user} = useContext(AppContext);
const TETHYS_SINGLE_APP_MODE = ["true", "True"].includes(
process.env.TETHYS_SINGLE_APP_MODE
);
const TETHYS_PORTAL_BASE = getTethysPortalBase();

const Header = ({ onNavChange }) => {
const { tethysApp, user } = useContext(AppContext);
const showNav = () => onNavChange(true);

return (
<>
<CustomNavBar fixed="top" bg="primary" variant="dark" className="shadow">
<Container as="header" fluid className="px-4">
<NavButton onClick={showNav}></NavButton>
<LinkContainer to="/">
<Navbar.Brand className="mx-0 d-none d-sm-block">
<img
src={tethysApp.icon}
width="30"
height="30"
className="d-inline-block align-top rounded-circle"
alt=""
/>{' ' + tethysApp.title}
</Navbar.Brand>
</LinkContainer>
<Form inline="true">
{user.isStaff &&
<HeaderButton href={tethysApp.settingsUrl} tooltipPlacement="bottom" tooltipText="Settings" className="me-2"><BsGear size="1.5rem"/></HeaderButton>
}
<HeaderButton href={tethysApp.exitUrl} tooltipPlacement="bottom" tooltipText="Exit"><BsX size="1.5rem"/></HeaderButton>
</Form>
</Container>
</CustomNavBar>
<CustomNavBar fixed="top" bg="primary" variant="dark" className="shadow">
<Container as="header" fluid className="px-4">
<NavButton onClick={showNav}></NavButton>
<LinkContainer to="/">
<Navbar.Brand className="mx-0 d-none d-sm-block">
<img
src={tethysApp.icon}
width="30"
height="30"
className="d-inline-block align-top rounded-circle"
alt=""
/>
{" " + tethysApp.title}
</Navbar.Brand>
</LinkContainer>
<Form className="d-flex align-items-center">
{user.isStaff && (
<HeaderButton
href={tethysApp.settingsUrl}
tooltipPlacement="bottom"
tooltipText="Settings"
className="me-2"
>
<BsGear size="1.5rem" />
</HeaderButton>
)}
{TETHYS_SINGLE_APP_MODE ? (
user.isAuthenticated ? (
<UserHeaderMenu
user={user}
gravatarUrl={user.gravatarUrl}
isStaff={user.isStaff}
/>
) : (
<HeaderButton
onClick={() => {
window.location.assign(
`${TETHYS_PORTAL_BASE}/accounts/login?next=${window.location.pathname}`
);
}}
tooltipPlacement="bottom"
tooltipText="Log In"
>
Log In
</HeaderButton>
)
) : (
<HeaderButton
href={tethysApp.exitUrl}
tooltipPlacement="bottom"
tooltipText="Exit"
>
<BsX size="1.5rem" />
</HeaderButton>
)}
</Form>
</Container>
</CustomNavBar>
</>
);
};
Expand All @@ -51,4 +94,4 @@ Header.propTypes = {
onNavChange: PropTypes.func,
};

export default Header;
export default Header;
Loading