Skip to content
Open
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
131 changes: 131 additions & 0 deletions src/actions/__tests__/admin-access-actions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* @jest-environment jsdom
*/
import configureStore from "redux-mock-store";
import thunk from "redux-thunk";
import flushPromises from "flush-promises";
import {
postRequest,
putRequest
} from "openstack-uicore-foundation/lib/utils/actions";
import { saveAdminAccess } from "../admin-access-actions";
import * as methods from "../../utils/methods";

jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({
__esModule: true,
...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"),
postRequest: jest.fn(),
putRequest: jest.fn()
}));

const requestMock =
(requestActionCreator, receiveActionCreator) => () => (dispatch) => {
if (requestActionCreator && typeof requestActionCreator === "function") {
dispatch(requestActionCreator({}));
}
return new Promise((resolve) => {
if (typeof receiveActionCreator === "function") {
dispatch(receiveActionCreator({ response: { id: 1 } }));
} else {
dispatch(receiveActionCreator);
}
resolve({ response: { id: 1 } });
});
};

const rejectMock = () => () => () => Promise.reject(new Error("API error"));

describe("saveAdminAccess", () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);

beforeEach(() => {
jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN");
postRequest.mockImplementation(requestMock);
putRequest.mockImplementation(requestMock);
});

afterEach(() => {
jest.restoreAllMocks();
});

describe("create path (entity has no id)", () => {
it("returns a Promise", async () => {
const store = mockStore({});
const result = store.dispatch(
saveAdminAccess({ title: "Group A", members: [], summits: [] })
);
expect(result).toBeInstanceOf(Promise);
await expect(result).resolves.toBeUndefined();
});

it("dispatches ADMIN_ACCESS_ADDED then STOP_LOADING on success", async () => {
const store = mockStore({});
store.dispatch(
saveAdminAccess({ title: "Group A", members: [], summits: [] })
);
await flushPromises();

const actionTypes = store.getActions().map((a) => a.type);
expect(actionTypes).toContain("ADMIN_ACCESS_ADDED");
expect(actionTypes).toContain("STOP_LOADING");
expect(actionTypes.indexOf("STOP_LOADING")).toBeGreaterThan(
actionTypes.indexOf("ADMIN_ACCESS_ADDED")
);
});

it("still dispatches STOP_LOADING when postRequest rejects", async () => {
postRequest.mockImplementation(rejectMock);
const store = mockStore({});
await store
.dispatch(
saveAdminAccess({ title: "Group A", members: [], summits: [] })
)
.catch(() => {});
await flushPromises();

const actionTypes = store.getActions().map((a) => a.type);
expect(actionTypes).toContain("STOP_LOADING");
});
});

describe("update path (entity has id)", () => {
it("returns a Promise", async () => {
const store = mockStore({});
const result = store.dispatch(
saveAdminAccess({ id: 1, title: "Group A", members: [], summits: [] })
);
expect(result).toBeInstanceOf(Promise);
await expect(result).resolves.toBeUndefined();
});

it("dispatches ADMIN_ACCESS_UPDATED then STOP_LOADING on success", async () => {
const store = mockStore({});
store.dispatch(
saveAdminAccess({ id: 1, title: "Group A", members: [], summits: [] })
);
await flushPromises();

const actionTypes = store.getActions().map((a) => a.type);
expect(actionTypes).toContain("ADMIN_ACCESS_UPDATED");
expect(actionTypes).toContain("STOP_LOADING");
expect(actionTypes.indexOf("STOP_LOADING")).toBeGreaterThan(
actionTypes.indexOf("ADMIN_ACCESS_UPDATED")
);
});

it("still dispatches STOP_LOADING when putRequest rejects", async () => {
putRequest.mockImplementation(rejectMock);
const store = mockStore({});
await store
.dispatch(
saveAdminAccess({ id: 1, title: "Group A", members: [], summits: [] })
)
.catch(() => {});
await flushPromises();

const actionTypes = store.getActions().map((a) => a.type);
expect(actionTypes).toContain("STOP_LOADING");
});
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests cover the happy-path dispatch sequence (ADDED/UPDATED → STOP_LOADING) but there is no test for the save failure path. A test that rejects postRequest/putRequest would confirm that stopLoading still fires via .finally() — a future refactor could silently break that guarantee.

96 changes: 48 additions & 48 deletions src/actions/admin-access-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@ import {
createAction,
stopLoading,
startLoading,
showMessage,
showSuccessMessage,
authErrorHandler,
escapeFilterValue
} from "openstack-uicore-foundation/lib/utils/actions";
import history from "../history";
import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions";
import { getAccessTokenSafely } from "../utils/methods";
import { DEFAULT_PER_PAGE } from "../utils/constants";

Expand Down Expand Up @@ -94,8 +91,8 @@ export const getAdminAccesses =
createAction(REQUEST_ADMIN_ACCESSES),
createAction(RECEIVE_ADMIN_ACCESSES),
`${window.API_BASE_URL}/api/v1/summit-administrator-groups`,
authErrorHandler,
{ order, orderDir, term }
snackbarErrorHandler,
{ order, orderDir, term, page, perPage }
Comment on lines +94 to +95

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) Inspect snackbar/auth error handler implementations.
fd -i 'base-actions.js' | xargs -r rg -n -A20 -B5 'snackbarErrorHandler|authErrorHandler'

# 2) Inspect request helper semantics (resolve vs reject on errors).
rg -n -A30 -B10 --type=js 'export const getRequest|function getRequest|const getRequest'
rg -n -A30 -B10 --type=js 'export const postRequest|export const putRequest|export const deleteRequest'

# 3) Re-check caller expectation in list page.
rg -n -A8 -B4 --type=js 'getAdminAccess\(' src/pages/admin_access/admin-access-list-page.js

Repository: fntechgit/summit-admin

Length of output: 2009


🏁 Script executed:

# Check request-helper or actions utility to understand error handler contract
rg -n -A15 'error.*handler|errorHandler' src/utils/request-helper.js src/utils/actions.js 2>/dev/null | head -100

# Inspect the actual admin-access-actions.js to see full context of lines 94-95, 118-119, 190
cat -n src/actions/admin-access-actions.js | sed -n '85,110p'
cat -n src/actions/admin-access-actions.js | sed -n '110,130p'
cat -n src/actions/admin-access-actions.js | sed -n '185,195p'

# Find how request utilities handle error callbacks (getRequest, postRequest, etc.)
fd -type f 'request-helper.js' | xargs -r cat -n | head -150

Repository: fntechgit/summit-admin

Length of output: 2321


🏁 Script executed:

# Find request-helper or actions utility files
find . -name "*request*" -o -name "*actions*" | grep -E "utils|lib" | head -20

# Look for getRequest implementation to understand error handler contract
rg -n "export.*getRequest|function getRequest" | head -20

# Check other error handler implementations for comparison
rg -n "ErrorHandler.*=" -A5 | head -100

Repository: fntechgit/summit-admin

Length of output: 5481


🏁 Script executed:

# Check package.json for openstack-uicore-foundation version
cat package.json | grep -A2 -B2 "openstack-uicore-foundation"

# Look for any local request helper/wrapper that might provide more context
find src -name "*request*" -o -name "*helper*" | grep -v node_modules

# Check admin-access-list-page to understand caller expectations
cat -n src/pages/admin_access/admin-access-list-page.js | sed -n '55,80p'

Repository: fntechgit/summit-admin

Length of output: 1720


🏁 Script executed:

# Look for any error handlers that explicitly return rejected promises or throw
rg -n "return.*reject|throw.*Error|Promise.reject" src/actions/ | head -30

# Check if any error handlers dispatch a rejection or error state
rg -n "ERROR|FAILURE" src/actions/admin-access-actions.js

# Look for patterns where handlers might suppress errors
rg -n "catch.*=>" src/actions/ | head -20

Repository: fntechgit/summit-admin

Length of output: 2140


🌐 Web query:

openstack-uicore-foundation getRequest error handler Promise rejection semantics

💡 Result:

The library openstack-uicore-foundation is a collection of ReactJS and Redux components designed for the OpenStack marketing website [1][2]. It is not a general-purpose OpenStack API client or a low-level SDK for managing OpenStack infrastructure [1][2]. There is no evidence of a "getRequest" function or specific "Promise rejection semantics" within the openstack-uicore-foundation repository [1][2]. Queries regarding "getRequest" and error handling in an OpenStack context typically relate to other projects, such as: - The OpenStack SDK (for Python, Rust, or other languages), which provides standardized methods for API interaction and error handling [3][4][5]. - OpenStack API specifications regarding structured error responses [6]. - Other frameworks like Uvicore (a separate Python framework) that explicitly manage exception handlers [7]. If you are encountering a "getRequest" error, please verify the library or SDK you are using to interact with the OpenStack API, as it is likely not openstack-uicore-foundation [1][2][3]. For standard JavaScript/TypeScript applications interacting with OpenStack APIs, standard Promise-based error handling (using.catch or try/catch blocks with async/await) is the recommended approach to managing rejections [1][2].

Citations:


🏁 Script executed:

# Check admin-access-actions test file for mocking patterns
cat -n src/actions/__tests__/admin-access-actions.test.js | head -80

# Look for other uses of getRequest to see error handler patterns
rg -n "getRequest" src/actions/ -B3 -A3 | head -150

# Check if there's documentation or type definitions for getRequest
find . -path ./node_modules -prune -o -name "*.d.ts" -exec grep -l "getRequest" {} \; 2>/dev/null | head -10

Repository: fntechgit/summit-admin

Length of output: 11707


🏁 Script executed:

# Get the complete saveAdminAccess function
cat -n src/actions/admin-access-actions.js | sed -n '128,175p'

Repository: fntechgit/summit-admin

Length of output: 1723


🏁 Script executed:

# Get the complete test case that checks rejection behavior
cat -n src/actions/__tests__/admin-access-actions.test.js | sed -n '77,95p'

Repository: fntechgit/summit-admin

Length of output: 837


🏁 Script executed:

# Search for authErrorHandler implementation in the codebase
rg -n "export const authErrorHandler|export function authErrorHandler" -A10

# Also check if there's a local implementation or if it's all from openstack-uicore-foundation
rg -n "authErrorHandler" src/ --type=js | head -20

Repository: fntechgit/summit-admin

Length of output: 1273


Confirm error handler doesn't suppress rejections in the request chain.

snackbarErrorHandler dispatches a message but doesn't explicitly return a rejected Promise. Verify that getRequest, putRequest, and deleteRequest from openstack-uicore-foundation properly re-throw or re-reject after invoking the error handler; otherwise:

  • AdminAccessListPage relies on getAdminAccess(accessId).catch(...) to redirect on failure, which won't trigger if the rejection is swallowed
  • getAdminAccess and deleteAdminAccess use .then(() => stopLoading()) which won't execute on error, leaving loading state stuck

Consider using .finally() pattern (as in saveAdminAccess) to ensure stopLoading() always executes regardless of request outcome.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/actions/admin-access-actions.js` around lines 94 - 95, The
snackbarErrorHandler may be swallowing promise rejections instead of propagating
them, causing error handling chains to break. In the getAdminAccess and
deleteAdminAccess functions, replace the `.then(() => stopLoading())` pattern
with a `.finally(() => stopLoading())` block to ensure the loading state is
cleared regardless of request success or failure, similar to how saveAdminAccess
already implements this pattern. Verify that snackbarErrorHandler explicitly
returns a rejected Promise after displaying the error message so that downstream
catch handlers in AdminAccessListPage and other callers can properly handle the
rejection chain.

)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Expand All @@ -118,7 +115,7 @@ export const getAdminAccess = (adminAccessId) => async (dispatch) => {
null,
createAction(RECEIVE_ADMIN_ACCESS),
`${window.API_BASE_URL}/api/v1/summit-administrator-groups/${adminAccessId}`,
authErrorHandler
snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Expand All @@ -128,52 +125,55 @@ export const resetAdminAccessForm = () => (dispatch) => {
dispatch(createAction(RESET_ADMIN_ACCESS_FORM)({}));
};

export const saveAdminAccess =
(entity, noAlert = false) =>
async (dispatch) => {
const accessToken = await getAccessTokenSafely();
export const saveAdminAccess = (entity) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();

dispatch(startLoading());
dispatch(startLoading());

const normalizedEntity = normalizeEntity(entity);
const params = { access_token: accessToken };

if (entity.id) {
putRequest(
createAction(UPDATE_ADMIN_ACCESS),
createAction(ADMIN_ACCESS_UPDATED),
`${window.API_BASE_URL}/api/v1/summit-administrator-groups/${entity.id}`,
normalizedEntity,
authErrorHandler,
entity
)(params)(dispatch).then(() => {
if (!noAlert)
dispatch(showSuccessMessage(T.translate("admin_access.saved")));
else dispatch(stopLoading());
});
} else {
const successMessage = {
title: T.translate("general.done"),
html: T.translate("admin_access.created"),
type: "success"
};

postRequest(
createAction(UPDATE_ADMIN_ACCESS),
createAction(ADMIN_ACCESS_ADDED),
`${window.API_BASE_URL}/api/v1/summit-administrator-groups`,
normalizedEntity,
authErrorHandler,
entity
)(params)(dispatch).then((payload) => {
const normalizedEntity = normalizeEntity(entity);
const params = { access_token: accessToken };

if (entity.id) {
return putRequest(
createAction(UPDATE_ADMIN_ACCESS),
createAction(ADMIN_ACCESS_UPDATED),
`${window.API_BASE_URL}/api/v1/summit-administrator-groups/${entity.id}`,
normalizedEntity,
snackbarErrorHandler,
entity
)(params)(dispatch)
.then(() => {
dispatch(
showMessage(successMessage, () => {
history.push(`/app/admin-access/${payload.response.id}`);
snackbarSuccessHandler({
title: T.translate("general.success"),
html: T.translate("admin_access.saved")
})
);
})
.finally(() => {
dispatch(stopLoading());
});
}
};
}
return postRequest(
createAction(UPDATE_ADMIN_ACCESS),
createAction(ADMIN_ACCESS_ADDED),
`${window.API_BASE_URL}/api/v1/summit-administrator-groups`,
normalizedEntity,
snackbarErrorHandler,
entity
)(params)(dispatch)
.then(() => {
dispatch(
snackbarSuccessHandler({
title: T.translate("general.success"),
html: T.translate("admin_access.created")
})
);
})
.finally(() => {
dispatch(stopLoading());
});
};

export const deleteAdminAccess = (adminAccessId) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();
Expand All @@ -187,7 +187,7 @@ export const deleteAdminAccess = (adminAccessId) => async (dispatch) => {
createAction(ADMIN_ACCESS_DELETED)({ adminAccessId }),
`${window.API_BASE_URL}/api/v1/summit-administrator-groups/${adminAccessId}`,
null,
authErrorHandler
snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Expand Down
Loading
Loading