Merge branch 'SIPS-Development' into feature/ithc-mitigation
This commit is contained in:
@@ -2,6 +2,7 @@ import _ from "lodash";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import { useRouter } from "next/router";
|
||||
import transLookup from "../../../data/lookuptranslations.json";
|
||||
import resolveCrmDisplayValue from "../../../lib/i18n/crmDisplay/resolveCrmDisplayValue";
|
||||
import SummaryCard from "../summary/components/SummaryCard";
|
||||
import SummaryRow from "../summary/components/SummaryRow";
|
||||
import { hasOwn } from "../../utils";
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/router";
|
||||
import transLookup from "../../../data/lookuptranslations.json";
|
||||
import SummaryCard from "../summary/components/SummaryCard";
|
||||
import SummaryRow from "../summary/components/SummaryRow";
|
||||
import resolveCrmDisplayValue from "../../../lib/i18n/crmDisplay/resolveCrmDisplayValue";
|
||||
import { hasOwn } from "../../utils";
|
||||
|
||||
const Pinswg_transportandworkactid = (props) => {
|
||||
|
||||
@@ -19,6 +19,40 @@ import { getPortalLogin } from "../../../actions/services/accountService";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const createVerificationTokenCompletion = () => {
|
||||
let complete;
|
||||
|
||||
const promise = new Promise((resolve) => {
|
||||
complete = resolve;
|
||||
});
|
||||
|
||||
return { promise, complete };
|
||||
};
|
||||
|
||||
const createVerificationTokenSynchronizationAdapter = (
|
||||
verificationTokenCompletion
|
||||
) => {
|
||||
const adapter = PrismaAdapter(prisma);
|
||||
|
||||
return {
|
||||
...adapter,
|
||||
|
||||
async createVerificationToken(data) {
|
||||
try {
|
||||
const result = await adapter.createVerificationToken(data);
|
||||
|
||||
verificationTokenCompletion.complete({ result });
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
verificationTokenCompletion.complete({ error });
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const WELSH_LANGUAGE_CODE = 846040000;
|
||||
|
||||
const authDiagnostic = (event, metadata = {}) => {
|
||||
@@ -167,6 +201,7 @@ const resolveRequestLocale = (req) => {
|
||||
|
||||
const authOptions = (req, res) => {
|
||||
const requestLocale = resolveRequestLocale(req);
|
||||
const verificationTokenCompletion = createVerificationTokenCompletion();
|
||||
|
||||
return {
|
||||
providers: [
|
||||
@@ -186,6 +221,13 @@ const authOptions = (req, res) => {
|
||||
effectiveLocale
|
||||
});
|
||||
|
||||
const tokenCreation =
|
||||
await verificationTokenCompletion.promise;
|
||||
|
||||
if (tokenCreation.error) {
|
||||
throw tokenCreation.error;
|
||||
}
|
||||
|
||||
return res
|
||||
.writeHead(200, { "Content-Type": "application/json" })
|
||||
.json({ url: formURL });
|
||||
@@ -240,7 +282,10 @@ const authOptions = (req, res) => {
|
||||
}
|
||||
})
|
||||
],
|
||||
adapter: PrismaAdapter(prisma),
|
||||
|
||||
adapter: createVerificationTokenSynchronizationAdapter(
|
||||
verificationTokenCompletion
|
||||
),
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
session: {
|
||||
strategy: "database",
|
||||
|
||||
+27
-8
@@ -317,11 +317,11 @@ export const getServerSideProps = wrapper.getServerSideProps(
|
||||
const limitRequests = pLimit(5);
|
||||
const arr = [];
|
||||
|
||||
const list =
|
||||
detailsType === "mySubmittedReps"
|
||||
? resultsObj
|
||||
: resultsObj.value;
|
||||
|
||||
const list = resultsObj.value;
|
||||
// detailsType === "mySubmittedReps"
|
||||
// ? resultsObj
|
||||
// : resultsObj.value;
|
||||
// console.log(list);
|
||||
const tasks = list.map((item) =>
|
||||
limitRequests(async () => {
|
||||
try {
|
||||
@@ -374,18 +374,37 @@ export const getServerSideProps = wrapper.getServerSideProps(
|
||||
return arr;
|
||||
};
|
||||
|
||||
const DASHBOARD_LIMIT = 3;
|
||||
|
||||
const limitResults = (data) => ({
|
||||
...data,
|
||||
value: (data?.value ?? []).slice(0, DASHBOARD_LIMIT)
|
||||
});
|
||||
|
||||
const [
|
||||
myCasesDetails,
|
||||
watchedCasesDetails,
|
||||
mySubmittedRepsDetails,
|
||||
myRepresentationsDetails
|
||||
] = await Promise.all([
|
||||
getDetails(myCases, "myCases"),
|
||||
getDetails(filteredWatchedCases, "myWatchedCases"),
|
||||
getDetails(mySubmittedReps, "mySubmittedReps"),
|
||||
getDetails(limitResults(myCases), "myCases"),
|
||||
getDetails(limitResults(filteredWatchedCases), "myWatchedCases"),
|
||||
getDetails(limitResults(mySubmittedReps), "mySubmittedReps"),
|
||||
getDetails(myRepresentations, "myRepresentations")
|
||||
]);
|
||||
|
||||
// const [
|
||||
// myCasesDetails,
|
||||
// watchedCasesDetails,
|
||||
// mySubmittedRepsDetails,
|
||||
// myRepresentationsDetails
|
||||
// ] = await Promise.all([
|
||||
// getDetails(limitResults(myCases), "myCases"),
|
||||
// getDetails(limitResults(filteredWatchedCases), "myWatchedCases"),
|
||||
// getDetails(limitResults(mySubmittedReps), "mySubmittedReps"),
|
||||
// getDetails(limitResults(myRepresentations), "myRepresentations")
|
||||
// ]);
|
||||
|
||||
store.dispatch(setAccountDetails(accountDetails));
|
||||
store.dispatch(setLoggedInUserId(loggedInUser));
|
||||
store.dispatch(setMyCases(myCases));
|
||||
|
||||
+327
-10
@@ -11,7 +11,63 @@ import ViewAll from "../../components/myportal/viewall";
|
||||
import NoSessionWarning from "../../components/nosession";
|
||||
import TimeOut from "../../components/timeout";
|
||||
import ServiceBanner from "../../components/myportal/servicebanner";
|
||||
import { getSession } from "next-auth/react";
|
||||
import pLimit from "p-limit";
|
||||
|
||||
import {
|
||||
getPersonalAccount,
|
||||
getPortalLogin
|
||||
} from "../../actions/services/accountService";
|
||||
|
||||
import { getPortalModuleDetails } from "../../actions/services/caseService";
|
||||
|
||||
import {
|
||||
getAwaitingSubmissionFromBlob,
|
||||
getRepsFromBlob
|
||||
} from "../../actions/services/documentService";
|
||||
|
||||
import {
|
||||
getMyCases,
|
||||
getMyLPACases,
|
||||
getWatchedCases
|
||||
} from "../../actions/services/portalService";
|
||||
|
||||
import { getBasicSearch } from "../../actions/services/searchService";
|
||||
|
||||
import { splitWatchedCasesBySubmissionState } from "../../lib/domain/dashboard-policy";
|
||||
|
||||
import { getFormCollectionByID } from "../../components/utils";
|
||||
|
||||
import {
|
||||
setAccountDetails,
|
||||
setContainerID,
|
||||
setLoggedInUserId
|
||||
} from "../../store/accountDetails/action";
|
||||
|
||||
import {
|
||||
setAwaitingSubmissionDetails,
|
||||
setAwaitingSubmissionFromBlob
|
||||
} from "../../store/awaitingSubmission/action";
|
||||
|
||||
import { setCurrentView } from "../../store/currentView/action";
|
||||
|
||||
import { setMyCases, setMyCasesDetails } from "../../store/myCases/action";
|
||||
|
||||
import {
|
||||
setMyRepresentations,
|
||||
setMyRepresentationsDetails,
|
||||
setMySubmittedReps,
|
||||
setMySubmittedRepsDetails
|
||||
} from "../../store/myRepresentations/action";
|
||||
|
||||
import {
|
||||
setWatchedCases,
|
||||
setWatchedCasesDetails
|
||||
} from "../../store/watchedCases/action";
|
||||
|
||||
import { setSearchResults } from "../../store/searchOutput/action";
|
||||
import { setSearch } from "../../store/search/action";
|
||||
import { wrapper } from "../../store/store";
|
||||
const Home = (props) => {
|
||||
const { footerLinks, pages } = props;
|
||||
let { t, lang } = useTranslation();
|
||||
@@ -39,7 +95,7 @@ const Home = (props) => {
|
||||
),
|
||||
"myRepresentations": t("common:myrepresentations-viewall-title"),
|
||||
"watchedCases": t("common:watchedcases-viewall-title"),
|
||||
"mySubmittedReps": t("common:mysubmittedreps-viewall-title"),
|
||||
"mySubmittedReps": t("common:mysubmittedreps-viewall-title")
|
||||
};
|
||||
|
||||
function whichTitle(key) {
|
||||
@@ -83,14 +139,275 @@ const Home = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
// export const getServerSideProps = wrapper.getServerSideProps(
|
||||
// (store) =>
|
||||
// async ({ req, res }) => {
|
||||
// const searchResultsObj = (await getBasicSearch("View All")) || null;
|
||||
// store.dispatch(setSearchResults(searchResultsObj));
|
||||
// store.dispatch(setSearch("View All"));
|
||||
// }
|
||||
// );
|
||||
export const getServerSideProps = wrapper.getServerSideProps(
|
||||
(store) => async (ctx) => {
|
||||
const { query } = ctx;
|
||||
|
||||
// The View All component uses router.query.key.
|
||||
const viewKey = Array.isArray(query.key) ? query.key[0] : query.key;
|
||||
console.log("[ViewAll SSR] Query:", ctx.query);
|
||||
console.log("[ViewAll SSR] Selected viewKey:", viewKey);
|
||||
const validViewKeys = [
|
||||
"myCases",
|
||||
"watchedCases",
|
||||
"mySubmittedReps",
|
||||
"myRepresentations",
|
||||
"awaitingSubmissionDetails"
|
||||
];
|
||||
|
||||
if (!validViewKeys.includes(viewKey)) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/myportal",
|
||||
permanent: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Authenticate before retrieving portal data.
|
||||
const thisSession = await getSession(ctx);
|
||||
|
||||
if (!thisSession?.user?.email) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/auth/signin",
|
||||
permanent: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const portalLogin = await getPortalLogin(thisSession.user.email);
|
||||
|
||||
const loggedInUser = portalLogin?.value?.[0]?.contactid;
|
||||
|
||||
if (!loggedInUser) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/auth/signin",
|
||||
permanent: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const accountDetails = await getPersonalAccount(loggedInUser);
|
||||
|
||||
if (!accountDetails || accountDetails.errorCode) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/myportal/error",
|
||||
permanent: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Preserve the LPA logic from MyPortal index.
|
||||
const isLPA = accountDetails.pinswg_typeofinvolvement === 846040012;
|
||||
|
||||
const lpaId =
|
||||
accountDetails[
|
||||
"pinswg_contact_associatedlpa@OData.Community.Display.V1.FormattedValue"
|
||||
];
|
||||
|
||||
// Full-list detail enrichment.
|
||||
// No dashboard limit is applied here.
|
||||
const getDetails = async (resultsObj, detailsType) => {
|
||||
const limitRequests = pLimit(5);
|
||||
|
||||
const list = Array.isArray(resultsObj)
|
||||
? resultsObj
|
||||
: (resultsObj?.value ?? []);
|
||||
|
||||
const tasks = list.map((item) =>
|
||||
limitRequests(async () => {
|
||||
try {
|
||||
let caseID = "";
|
||||
|
||||
switch (detailsType) {
|
||||
case "myCases":
|
||||
caseID = item.ticketnumber;
|
||||
break;
|
||||
|
||||
case "myWatchedCases":
|
||||
case "mySubmittedReps":
|
||||
caseID =
|
||||
item[
|
||||
"_pinswg_watchedcase_value@OData.Community.Display.V1.FormattedValue"
|
||||
];
|
||||
break;
|
||||
|
||||
case "awaitingSubmission":
|
||||
caseID = item.ticketnumber;
|
||||
break;
|
||||
|
||||
case "myRepresentations":
|
||||
caseID = item.casereference;
|
||||
break;
|
||||
|
||||
default:
|
||||
caseID = item.pinswg_title;
|
||||
}
|
||||
|
||||
if (!item.pinswg_appealcasetype || !caseID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formCollection = getFormCollectionByID(
|
||||
item.pinswg_appealcasetype
|
||||
);
|
||||
|
||||
if (!formCollection?.LogicalCollectionName) {
|
||||
console.error(
|
||||
"Missing collection metadata for appeal type:",
|
||||
item.pinswg_appealcasetype
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
(await getPortalModuleDetails(
|
||||
formCollection.LogicalCollectionName,
|
||||
caseID
|
||||
)) || {}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Error fetching View All details:", err);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const details = await Promise.all(tasks);
|
||||
|
||||
// Retain the original result ordering.
|
||||
return details.filter(Boolean);
|
||||
};
|
||||
|
||||
// Shared account state.
|
||||
store.dispatch(setAccountDetails(accountDetails));
|
||||
store.dispatch(setLoggedInUserId(loggedInUser));
|
||||
store.dispatch(setContainerID(thisSession.user.id));
|
||||
|
||||
// Ensure the rendered View All category agrees
|
||||
// with the category loaded by this request.
|
||||
store.dispatch(
|
||||
setCurrentView({
|
||||
viewKey,
|
||||
viewName: viewKey
|
||||
})
|
||||
);
|
||||
|
||||
switch (viewKey) {
|
||||
case "myCases": {
|
||||
const myCases = isLPA
|
||||
? await getMyLPACases(lpaId)
|
||||
: await getMyCases(loggedInUser);
|
||||
|
||||
const myCasesDetails = await getDetails(myCases, "myCases");
|
||||
|
||||
store.dispatch(setMyCases(myCases));
|
||||
store.dispatch(setMyCasesDetails(myCasesDetails));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "watchedCases": {
|
||||
const watchedCases = await getWatchedCases(loggedInUser);
|
||||
|
||||
const { watchedCases: filteredWatchedCases } =
|
||||
splitWatchedCasesBySubmissionState(
|
||||
watchedCases.value ?? []
|
||||
);
|
||||
|
||||
const watchedCasesDetails = await getDetails(
|
||||
filteredWatchedCases,
|
||||
"myWatchedCases"
|
||||
);
|
||||
|
||||
store.dispatch(setWatchedCases(filteredWatchedCases));
|
||||
|
||||
store.dispatch(setWatchedCasesDetails(watchedCasesDetails));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "mySubmittedReps": {
|
||||
const watchedCases = await getWatchedCases(loggedInUser);
|
||||
|
||||
const { submittedRepresentations: mySubmittedReps } =
|
||||
splitWatchedCasesBySubmissionState(
|
||||
watchedCases.value ?? []
|
||||
);
|
||||
|
||||
const mySubmittedRepsDetails = await getDetails(
|
||||
mySubmittedReps,
|
||||
"mySubmittedReps"
|
||||
);
|
||||
|
||||
store.dispatch(setMySubmittedReps(mySubmittedReps));
|
||||
|
||||
store.dispatch(
|
||||
setMySubmittedRepsDetails(mySubmittedRepsDetails)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "myRepresentations": {
|
||||
const myRepresentations = await getRepsFromBlob(
|
||||
thisSession.user.id
|
||||
);
|
||||
|
||||
// The existing View All component reads
|
||||
// representation display data directly
|
||||
// from myRepresentations.value.
|
||||
store.dispatch(setMyRepresentations(myRepresentations));
|
||||
|
||||
// Preserve the existing Redux detail
|
||||
// collection contract.
|
||||
const myRepresentationsDetails = await getDetails(
|
||||
myRepresentations,
|
||||
"myRepresentations"
|
||||
);
|
||||
|
||||
store.dispatch(
|
||||
setMyRepresentationsDetails(myRepresentationsDetails)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "awaitingSubmissionDetails": {
|
||||
const awaitingSubmissionFromBlob =
|
||||
await getAwaitingSubmissionFromBlob(thisSession.user.id);
|
||||
|
||||
// Awaiting submission already has its
|
||||
// own Blob-backed data structure.
|
||||
// Preserve the dashboard's existing
|
||||
// Redux dispatches.
|
||||
store.dispatch(
|
||||
setAwaitingSubmissionFromBlob(awaitingSubmissionFromBlob)
|
||||
);
|
||||
|
||||
store.dispatch(
|
||||
setAwaitingSubmissionDetails(awaitingSubmissionFromBlob)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve the original View All search state.
|
||||
const searchResultsObj = (await getBasicSearch("View All")) || null;
|
||||
|
||||
store.dispatch(setSearchResults(searchResultsObj));
|
||||
|
||||
store.dispatch(setSearch("View All"));
|
||||
|
||||
return {
|
||||
props: {}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
@@ -105,7 +422,7 @@ const mapStateToProps = (state) => {
|
||||
myRepresentations: state.myRepresentations,
|
||||
awaitingSubmission: state.awaitingSubmission,
|
||||
currentView: state.currentView,
|
||||
mySubmittedReps: state.myRepresentations.mySubmittedReps,
|
||||
mySubmittedReps: state.myRepresentations.mySubmittedReps
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+15
-15
@@ -31,7 +31,7 @@ const appReducer = combineReducers({
|
||||
myCases,
|
||||
myRepresentations,
|
||||
watchedCases,
|
||||
form: formReducer,
|
||||
form: formReducer
|
||||
});
|
||||
|
||||
const combinedReducer = (state, action) => {
|
||||
@@ -53,28 +53,28 @@ const reducer = (state = {}, action) => {
|
||||
...state,
|
||||
server: {
|
||||
...state.server,
|
||||
progress: action.payload,
|
||||
},
|
||||
progress: action.payload
|
||||
}
|
||||
};
|
||||
case "CLIENT_ACTION":
|
||||
return {
|
||||
...state,
|
||||
client: {
|
||||
...state.client,
|
||||
progress: action.payload,
|
||||
},
|
||||
progress: action.payload
|
||||
}
|
||||
};
|
||||
case "GET_ITEMS":
|
||||
return {
|
||||
...state,
|
||||
items: action.payload,
|
||||
loading: false,
|
||||
loading: false
|
||||
};
|
||||
case "SET_ITEMS":
|
||||
return {
|
||||
...state,
|
||||
items: action.payload,
|
||||
loading: false,
|
||||
loading: false
|
||||
};
|
||||
default:
|
||||
return combinedReducer(state, action);
|
||||
@@ -84,7 +84,7 @@ const reducer = (state = {}, action) => {
|
||||
const migrations = {
|
||||
1: (state) => {
|
||||
return {
|
||||
...state,
|
||||
...state
|
||||
};
|
||||
},
|
||||
2: (state) => {
|
||||
@@ -93,12 +93,12 @@ const migrations = {
|
||||
const newStateObj = {
|
||||
...state,
|
||||
"currentView": {},
|
||||
"searchResultsObj": {},
|
||||
"searchResultsObj": {}
|
||||
};
|
||||
|
||||
console.info("v2 schema migration:" + JSON.stringify(newStateObj));
|
||||
return newStateObj;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// BINDING MIDDLEWARE
|
||||
@@ -119,7 +119,7 @@ const makeStore = ({ isServer }) => {
|
||||
const {
|
||||
persistStore,
|
||||
persistReducer,
|
||||
createMigrate,
|
||||
createMigrate
|
||||
} = require("redux-persist");
|
||||
const storage = require("redux-persist/lib/storage").default;
|
||||
|
||||
@@ -132,15 +132,15 @@ const makeStore = ({ isServer }) => {
|
||||
"LPAData",
|
||||
"accountDetails",
|
||||
"awaitingSubmission",
|
||||
"myCases",
|
||||
// "myCases",
|
||||
"myRepresentations",
|
||||
"watchedCases",
|
||||
"form",
|
||||
"form"
|
||||
],
|
||||
version: process.env.BUILD_ID_ENV,
|
||||
storage: storage,
|
||||
stateReconciler: autoMergeLevel2, // if needed, use a safer storage
|
||||
migrate: createMigrate(migrations, { debug: true }),
|
||||
migrate: createMigrate(migrations, { debug: true })
|
||||
};
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, reducer); // Create a new reducer with our existing reducer
|
||||
@@ -157,7 +157,7 @@ const makeStore = ({ isServer }) => {
|
||||
};
|
||||
export const setClientState = (clientState) => ({
|
||||
type: SET_CLIENT_STATE,
|
||||
payload: clientState,
|
||||
payload: clientState
|
||||
});
|
||||
// Export the wrapper & wrap the pages/_app.js with this wrapper only
|
||||
export const wrapper = createWrapper(makeStore);
|
||||
|
||||
@@ -6,7 +6,7 @@ const vm = require("vm");
|
||||
const tests = [];
|
||||
const test = (name, fn) => tests.push({ name, fn });
|
||||
|
||||
const loadAuthInternals = () => {
|
||||
const loadAuthInternals = ({ adapterFactory = () => ({}) } = {}) => {
|
||||
const filePath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
@@ -49,12 +49,14 @@ const loadAuthInternals = () => {
|
||||
NEXTAUTH_SECRET: "test-secret"
|
||||
}
|
||||
},
|
||||
PrismaAdapter: () => ({}),
|
||||
PrismaAdapter: () => adapterFactory(),
|
||||
PrismaClient: function PrismaClient() {
|
||||
return {};
|
||||
},
|
||||
NextAuth: () => ({}),
|
||||
EmailProvider: () => ({}),
|
||||
getPortalLogin: async () => ({ value: [] }),
|
||||
redactSensitive: (value) => value,
|
||||
consoleLogger: () => {},
|
||||
console: {
|
||||
log: () => {},
|
||||
@@ -68,6 +70,42 @@ const loadAuthInternals = () => {
|
||||
return context.module.exports;
|
||||
};
|
||||
|
||||
const deferred = () => {
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise;
|
||||
reject = rejectPromise;
|
||||
});
|
||||
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
const createResponse = () => {
|
||||
const calls = [];
|
||||
const response = {
|
||||
writeHead(status, headers) {
|
||||
calls.push({ type: "writeHead", status, headers });
|
||||
return response;
|
||||
},
|
||||
json(body) {
|
||||
calls.push({ type: "json", body });
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
return { response, calls };
|
||||
};
|
||||
|
||||
const createEmailApiRequest = () => ({
|
||||
query: { locale: "en" },
|
||||
body: {},
|
||||
cookies: {}
|
||||
});
|
||||
|
||||
const verificationUrl = (token) =>
|
||||
`https://english.example/api/auth/callback/emailAPI?callbackUrl=https%3A%2F%2Fenglish.example&token=${token}&email=user%40example.com`;
|
||||
|
||||
test("auth/resolveRequestLocale prefers query then body then cookie then default", async () => {
|
||||
const mod = loadAuthInternals();
|
||||
|
||||
@@ -144,6 +182,130 @@ test("auth/redirect callback rewrites external URL to locale-safe base origin",
|
||||
assert.strictEqual(parsed.searchParams.get("token"), "abc");
|
||||
});
|
||||
|
||||
test("auth/emailAPI waits for successful token persistence before returning its URL", async () => {
|
||||
const persistence = deferred();
|
||||
const mod = loadAuthInternals({
|
||||
adapterFactory: () => ({
|
||||
createVerificationToken: () => persistence.promise
|
||||
})
|
||||
});
|
||||
const { response, calls } = createResponse();
|
||||
const options = mod.authOptions(createEmailApiRequest(), response);
|
||||
const provider = options.providers[0];
|
||||
|
||||
const sendPromise = provider.sendVerificationRequest({
|
||||
identifier: "user@example.com",
|
||||
url: verificationUrl("token-one")
|
||||
});
|
||||
const createPromise = options.adapter.createVerificationToken({
|
||||
identifier: "user@example.com",
|
||||
token: "stored-token-one",
|
||||
expires: new Date(Date.now() + 60_000)
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
assert.deepStrictEqual(calls, []);
|
||||
|
||||
persistence.resolve({
|
||||
identifier: "user@example.com",
|
||||
token: "stored-token-one",
|
||||
expires: new Date(Date.now() + 60_000)
|
||||
});
|
||||
|
||||
await Promise.all([sendPromise, createPromise]);
|
||||
|
||||
assert.strictEqual(calls[0].type, "writeHead");
|
||||
assert.strictEqual(calls[0].status, 200);
|
||||
assert.strictEqual(calls[1].type, "json");
|
||||
assert.strictEqual(calls[1].body.url, verificationUrl("token-one"));
|
||||
});
|
||||
|
||||
test("auth/emailAPI does not return a verification URL when persistence fails", async () => {
|
||||
const persistence = deferred();
|
||||
const persistenceError = new Error("persistence failed");
|
||||
const mod = loadAuthInternals({
|
||||
adapterFactory: () => ({
|
||||
createVerificationToken: () => persistence.promise
|
||||
})
|
||||
});
|
||||
const { response, calls } = createResponse();
|
||||
const options = mod.authOptions(createEmailApiRequest(), response);
|
||||
|
||||
const sendPromise = options.providers[0].sendVerificationRequest({
|
||||
identifier: "user@example.com",
|
||||
url: verificationUrl("token-two")
|
||||
});
|
||||
const createPromise = options.adapter.createVerificationToken({
|
||||
identifier: "user@example.com",
|
||||
token: "stored-token-two",
|
||||
expires: new Date(Date.now() + 60_000)
|
||||
});
|
||||
|
||||
persistence.reject(persistenceError);
|
||||
|
||||
const [sendResult, createResult] = await Promise.allSettled([
|
||||
sendPromise,
|
||||
createPromise
|
||||
]);
|
||||
|
||||
assert.strictEqual(sendResult.status, "rejected");
|
||||
assert.strictEqual(sendResult.reason, persistenceError);
|
||||
assert.strictEqual(createResult.status, "rejected");
|
||||
assert.strictEqual(createResult.reason, persistenceError);
|
||||
assert.deepStrictEqual(calls, []);
|
||||
});
|
||||
|
||||
test("auth/emailAPI keeps concurrent sign-in persistence signals request-scoped", async () => {
|
||||
const firstPersistence = deferred();
|
||||
const secondPersistence = deferred();
|
||||
const adapters = [
|
||||
{ createVerificationToken: () => firstPersistence.promise },
|
||||
{ createVerificationToken: () => secondPersistence.promise }
|
||||
];
|
||||
const mod = loadAuthInternals({
|
||||
adapterFactory: () => adapters.shift()
|
||||
});
|
||||
const firstResponse = createResponse();
|
||||
const secondResponse = createResponse();
|
||||
const firstOptions = mod.authOptions(
|
||||
createEmailApiRequest(),
|
||||
firstResponse.response
|
||||
);
|
||||
const secondOptions = mod.authOptions(
|
||||
createEmailApiRequest(),
|
||||
secondResponse.response
|
||||
);
|
||||
|
||||
const firstSend = firstOptions.providers[0].sendVerificationRequest({
|
||||
identifier: "user@example.com",
|
||||
url: verificationUrl("first-token")
|
||||
});
|
||||
const firstCreate = firstOptions.adapter.createVerificationToken({});
|
||||
const secondSend = secondOptions.providers[0].sendVerificationRequest({
|
||||
identifier: "user@example.com",
|
||||
url: verificationUrl("second-token")
|
||||
});
|
||||
const secondCreate = secondOptions.adapter.createVerificationToken({});
|
||||
|
||||
secondPersistence.resolve({ token: "stored-second-token" });
|
||||
await Promise.all([secondSend, secondCreate]);
|
||||
|
||||
assert.deepStrictEqual(firstResponse.calls, []);
|
||||
assert.strictEqual(
|
||||
secondResponse.calls[1].body.url,
|
||||
verificationUrl("second-token")
|
||||
);
|
||||
|
||||
firstPersistence.resolve({ token: "stored-first-token" });
|
||||
await Promise.all([firstSend, firstCreate]);
|
||||
|
||||
assert.strictEqual(
|
||||
firstResponse.calls[1].body.url,
|
||||
verificationUrl("first-token")
|
||||
);
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
let passed = 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user