Skip to content

Commit

Permalink
Merge branch 'DSEGOG-361-view-all-users' into DSEGOG-362-add-a-user
Browse files Browse the repository at this point in the history
  • Loading branch information
joshdimanteto committed Feb 5, 2025
2 parents 262fb82 + 1b1e3b2 commit c529540
Show file tree
Hide file tree
Showing 42 changed files with 899 additions and 861 deletions.
24 changes: 17 additions & 7 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
run: yarn test
- name: Upload unit test coverage
if: success()
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
Expand Down Expand Up @@ -77,7 +77,7 @@ jobs:
retention-days: 10
playwright-tests-real:
name: E2E Tests
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

Expand All @@ -92,18 +92,18 @@ jobs:
- name: Install python-ldap dependencies
run: |
sudo apt-get update
sudo apt-get install -y libsasl2-dev python3.9-dev libldap2-dev libssl-dev
sudo apt-get install -y libsasl2-dev libldap2-dev libssl-dev
# Setup Python and environment dependencies (via cache)
- name: Setup Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5
with:
python-version: 3.9
python-version: 3.11
- name: Load Pip cache
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4
with:
path: ~/.cache/pip
key: ubuntu-20.04-pip-3.9-${{ env.pythonLocation }}-${{ hashFiles('operationsgateway-api/.github/ci_requirements.txt') }}
key: ${{ runner.os }}-pip-3.11-${{ env.pythonLocation }}-${{ hashFiles('operationsgateway-api/.github/ci_requirements.txt') }}
- name: Install Poetry
run: pip install -r .github/ci_requirements.txt
working-directory: ./operationsgateway-api
Expand All @@ -112,7 +112,17 @@ jobs:
- name: Start MongoDB
uses: supercharge/mongodb-github-action@5a87bd81f88e2a8b195f8b7b656f5cda1350815a # 1.11.0
with:
mongodb-version: '5.0'
mongodb-version: '7.0'

# Used to install mongoimport when Ubuntu 22.04 is used, identified at https://github.com/actions/runner-images/issues/6626#issuecomment-1327744126
- name: Install MongoDB Database Tools
run: |
sudo apt-get update
sudo apt-get install -y wget gnupg
wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt-get update
sudo apt-get install -y mongodb-database-tools
# Configure correct paths in config files
- name: Configure private key path
Expand All @@ -138,7 +148,7 @@ jobs:
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4
with:
path: ~/.cache/pypoetry/virtualenvs
key: ubuntu-20.04-poetry-3.9-${{ env.pythonLocation }}-${{ hashFiles('poetry.lock') }}
key: ${{ runner.os }}-poetry-3.11-${{ env.pythonLocation }}-${{ hashFiles('poetry.lock') }}
- name: Install dependencies
run: poetry install --without simulated-data
working-directory: ./operationsgateway-api
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"react-router-dom": "7.1.1",
"single-spa-react": "6.0.2",
"typescript": "5.6.2",
"vite": "5.4.11",
"vite": "5.4.12",
"zod": "^3.23.8"
},
"resolutions": {
Expand Down Expand Up @@ -99,7 +99,7 @@
"@testing-library/user-event": "14.5.2",
"@types/eslint-plugin-jsx-a11y": "6.10.0",
"@typescript-eslint/typescript-estree": "8.18.0",
"@vitest/coverage-v8": "2.1.8",
"@vitest/coverage-v8": "2.1.9",
"cross-env": "7.0.3",
"cypress": "13.16.0",
"cypress-delete-downloads-folder": "0.0.5",
Expand All @@ -112,16 +112,16 @@
"eslint-plugin-react": "7.37.2",
"eslint-plugin-react-hooks": "5.1.0",
"eslint-plugin-testing-library": "7.1.1",
"express": "4.21.0",
"express": "4.21.2",
"globals": "15.13.0",
"jsdom": "25.0.1",
"lint-staged": "15.2.0",
"prettier": "3.4.2",
"serve": "14.2.0",
"serve-static": "1.16.0",
"start-server-and-test": "2.0.0",
"start-server-and-test": "2.0.10",
"typescript-eslint": "8.18.0",
"vitest": "2.1.8",
"vitest": "2.1.9",
"vitest-canvas-mock": "0.3.3",
"vitest-fail-on-console": "0.7.1"
},
Expand Down
23 changes: 19 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,28 @@ import {
QueryClientProvider,
} from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AxiosError } from 'axios';
import React from 'react';
import { connect, Provider } from 'react-redux';
import { createBrowserRouter, Outlet, RouterProvider } from 'react-router';
import UsersTable from './admin/users/usersTable.component';
import {
clearFailedAuthRequestsQueue,
retryFailedAuthRequests,
} from './api/api';
import './App.css';
import { MicroFrontendId } from './app.types';
import handleOG_APIError from './handleOG_APIError';
import OGThemeProvider from './ogThemeProvider.component';
import PageNotFoundComponent from './pageNotFound/pageNotFound.component';
import Preloader from './preloader/preloader.component';
import retryOG_APIErrors from './retryOG_APIErrors';
import SettingsMenuItems from './settingsMenuItems.component';
import { requestPluginRerender } from './state/scigateway.actions';
import {
broadcastSignOut,
requestPluginRerender,
tokenRefreshed,
} from './state/scigateway.actions';
import { configureApp } from './state/slices/configSlice';
import { RootState, store } from './state/store';
import ViewTabs from './views/viewTabs.component';
Expand All @@ -32,12 +43,15 @@ const queryClient = new QueryClient({
queries: {
refetchOnWindowFocus: true,
staleTime: 300000,
retry: (failureCount, error) => {
return retryOG_APIErrors(failureCount, error as AxiosError);
},
},
},
// TODO: implement proper error handling

queryCache: new QueryCache({
onError: (error) => {
console.log('Got error ' + error.message);
handleOG_APIError(error as AxiosError);
},
}),
});
Expand Down Expand Up @@ -67,7 +81,8 @@ const Layout: React.FunctionComponent = () => {
const action = (e as CustomEvent).detail;
if (requestPluginRerender.match(action)) {
forceUpdate();
}
} else if (tokenRefreshed.match(action)) retryFailedAuthRequests();
else if (broadcastSignOut.match(action)) clearFailedAuthRequestsQueue();
}

React.useEffect(() => {
Expand Down
56 changes: 16 additions & 40 deletions src/admin/users/userDialogue.component.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { screen } from '@testing-library/react';
import userEvent, { UserEvent } from '@testing-library/user-event';
import axios from 'axios';
import { MockInstance } from 'vitest';
import { ogApi } from '../../api/api';
import { renderComponentWithProviders } from '../../testUtils';
import UserDialogue, { UserDialogueProps } from './userDialogue.component';

Expand All @@ -26,7 +26,7 @@ describe('userDialogue', () => {
let axiosPostSpy: MockInstance;

beforeEach(() => {
axiosPostSpy = vi.spyOn(axios, 'post');
axiosPostSpy = vi.spyOn(ogApi, 'post');
});
afterEach(() => {
vi.clearAllMocks();
Expand Down Expand Up @@ -78,20 +78,12 @@ describe('userDialogue', () => {

await user.click(screen.getByText('Submit'));

expect(axiosPostSpy).toHaveBeenCalledWith(
'/users',
{
_id: 'new_user',
auth_type: 'local',
authorised_routes: ['/submit/hdf POST', '/users PATCH'],
sha256_password: 'secure_password',
},
{
headers: {
Authorization: 'Bearer null',
},
}
);
expect(axiosPostSpy).toHaveBeenCalledWith('/users', {
_id: 'new_user',
auth_type: 'local',
authorised_routes: ['/submit/hdf POST', '/users PATCH'],
sha256_password: 'secure_password',
});
});

it('adds user successfully (fedId)', async () => {
Expand All @@ -106,18 +98,10 @@ describe('userDialogue', () => {

await user.click(screen.getByText('Submit'));

expect(axiosPostSpy).toHaveBeenCalledWith(
'/users',
{
_id: 'new_user',
auth_type: 'FedID',
},
{
headers: {
Authorization: 'Bearer null',
},
}
);
expect(axiosPostSpy).toHaveBeenCalledWith('/users', {
_id: 'new_user',
auth_type: 'FedID',
});
});

it('adds user successfully (fedId) switch from local to fedId', async () => {
Expand All @@ -134,18 +118,10 @@ describe('userDialogue', () => {

await user.click(screen.getByText('Submit'));

expect(axiosPostSpy).toHaveBeenCalledWith(
'/users',
{
_id: 'new_user',
auth_type: 'FedID',
},
{
headers: {
Authorization: 'Bearer null',
},
}
);
expect(axiosPostSpy).toHaveBeenCalledWith('/users', {
_id: 'new_user',
auth_type: 'FedID',
});
});

it('displays error when adding a user without a password for "local" auth_type', async () => {
Expand Down
88 changes: 88 additions & 0 deletions src/api/api.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import axios from 'axios';
import { MicroFrontendId, type APIError } from '../app.types';
import { readSciGatewayToken } from '../parseTokens';
import { settings } from '../settings';
import { InvalidateTokenType } from '../state/scigateway.actions';

// These are for ensuring refresh request is only sent once when multiple requests
// are failing due to 403's at the same time
let isFetchingAccessToken = false;
let failedAuthRequestQueue: ((shouldReject?: boolean) => void)[] = [];

/* This should be called when SciGateway successfully refreshes the access token - it retries
all requests that failed due to an invalid token */
export const retryFailedAuthRequests = () => {
isFetchingAccessToken = false;
failedAuthRequestQueue.forEach((callback) => callback());
failedAuthRequestQueue = [];
};

/* This should be called when SciGateway logs out as would occur if a token refresh fails
due to the refresh token being out of date - it rejects all active request promises that
were awaiting a token refresh using the original error that occurred on the first attempt */
export const clearFailedAuthRequestsQueue = () => {
isFetchingAccessToken = false;
failedAuthRequestQueue.forEach((callback) => callback(true));
failedAuthRequestQueue = [];
};

export const ogApi = axios.create();

ogApi.interceptors.request.use(async (config) => {
const settingsData = await settings;
config.baseURL = settingsData ? settingsData.apiUrl : '';
config.headers['Authorization'] = `Bearer ${readSciGatewayToken()}`;
return config;
});

ogApi.interceptors.response.use(
(response) => response,
(error) => {
const originalRequest = error.config;

const errorDetail = (error.response.data as APIError)?.detail;

const errorMessage =
typeof errorDetail === 'string'
? errorDetail.toLocaleLowerCase()
: error.message;

// Check if the token is invalid and needs refreshing
// only allow a request to be retried once. Don't retry if not logged
// in, it should not have been accessible
if (
error.response?.status === 403 &&
errorMessage.includes('invalid token') &&
!originalRequest._retried &&
localStorage.getItem('scigateway:token')
) {
originalRequest._retried = true;

// Prevent other requests from also attempting to refresh while waiting for
// SciGateway to refresh the token
if (!isFetchingAccessToken) {
isFetchingAccessToken = true;

// Request SciGateway to refresh the token
document.dispatchEvent(
new CustomEvent(MicroFrontendId, {
detail: {
type: InvalidateTokenType,
},
})
);
}

// Add request to queue to be resolved only once SciGateway has successfully
// refreshed the token
return new Promise((resolve, reject) => {
failedAuthRequestQueue.push((shouldReject?: boolean) => {
if (shouldReject) reject(error);
else resolve(ogApi(originalRequest));
});
});
}
// Any other error
else return Promise.reject(error);
}
);
12 changes: 0 additions & 12 deletions src/api/channels.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,6 @@ describe('channels api functions', () => {

expect(result.current.data).toEqual([]);
});

it.todo(
'sends axios request to fetch channels and throws an appropriate error on failure'
);
});

describe('getScalarChannels', () => {
Expand Down Expand Up @@ -310,10 +306,6 @@ describe('channels api functions', () => {

expect(result.current.data).toEqual([]);
});

it.todo(
'sends axios request to fetch records and throws an appropriate error on failure'
);
});

describe('useChannelSummary', () => {
Expand Down Expand Up @@ -363,10 +355,6 @@ describe('channels api functions', () => {
expect(result.current.isPending).toBeTruthy();
expect(requestSent).toBe(false);
});

it.todo(
'sends axios request to fetch records and throws an appropriate error on failure'
);
});

describe('useScalarChannels', () => {
Expand Down
Loading

0 comments on commit c529540

Please sign in to comment.