mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-26 05:51:17 -07:00
Fix: prevent omada race conditions with auth and cookie caching (#6549)
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
This commit is contained in:
@@ -7,7 +7,10 @@ export function setCookieHeader(url, params) {
|
|||||||
const existingCookie = cookieJar.getCookieStringSync(url.toString());
|
const existingCookie = cookieJar.getCookieStringSync(url.toString());
|
||||||
if (existingCookie) {
|
if (existingCookie) {
|
||||||
params.headers = params.headers ?? {};
|
params.headers = params.headers ?? {};
|
||||||
params.headers[params.cookieHeader ?? "Cookie"] = existingCookie;
|
const cookieHeader = params.cookieHeader ?? "Cookie";
|
||||||
|
if (!params.headers[cookieHeader]) {
|
||||||
|
params.headers[cookieHeader] = existingCookie;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,4 +42,16 @@ describe("utils/proxy/cookie-jar", () => {
|
|||||||
|
|
||||||
expect(params.headers.Cookie).toContain("c=d");
|
expect(params.headers.Cookie).toContain("c=d");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not overwrite an explicit cookie header", async () => {
|
||||||
|
const { addCookieToJar, setCookieHeader } = await import("./cookie-jar");
|
||||||
|
|
||||||
|
const url = new URL("http://example4.test/path");
|
||||||
|
addCookieToJar(url, { "set-cookie": ["sid=1; Path=/"] });
|
||||||
|
|
||||||
|
const params = { headers: { Cookie: "manual=1" } };
|
||||||
|
setCookieHeader(url, params);
|
||||||
|
|
||||||
|
expect(params.headers.Cookie).toBe("manual=1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+274
-168
@@ -1,12 +1,40 @@
|
|||||||
|
import cache from "memory-cache";
|
||||||
|
|
||||||
import getServiceWidget from "utils/config/service-helpers";
|
import getServiceWidget from "utils/config/service-helpers";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { httpProxy } from "utils/proxy/http";
|
import { httpProxy } from "utils/proxy/http";
|
||||||
|
|
||||||
const proxyName = "omadaProxyHandler";
|
const proxyName = "omadaProxyHandler";
|
||||||
|
const sessionCacheKey = `${proxyName}__session`;
|
||||||
|
|
||||||
const logger = createLogger(proxyName);
|
const logger = createLogger(proxyName);
|
||||||
|
|
||||||
async function login(loginUrl, username, password, controllerVersionMajor) {
|
function getSessionCacheId(group, service, index) {
|
||||||
|
return [sessionCacheKey, group, service, index ?? "0"].join(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRetryWithFreshSession(status, responseData, attempt, usedCachedSession) {
|
||||||
|
return attempt === 0 && usedCachedSession && (status === 401 || status === 403 || responseData?.errorCode > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookieHeader(responseHeaders) {
|
||||||
|
const setCookie = responseHeaders?.["set-cookie"];
|
||||||
|
if (!setCookie) return null;
|
||||||
|
|
||||||
|
const cookies = new Map();
|
||||||
|
(Array.isArray(setCookie) ? setCookie : [setCookie]).forEach((cookie) => {
|
||||||
|
const cookiePair = cookie.split(";")[0];
|
||||||
|
if (!cookiePair) return;
|
||||||
|
|
||||||
|
const separatorIndex = cookiePair.indexOf("=");
|
||||||
|
const cookieName = separatorIndex === -1 ? cookiePair : cookiePair.slice(0, separatorIndex);
|
||||||
|
cookies.set(cookieName, cookiePair);
|
||||||
|
});
|
||||||
|
|
||||||
|
return cookies.size > 0 ? Array.from(cookies.values()).join("; ") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(loginUrl, username, password, controllerVersionMajor, sessionCacheId) {
|
||||||
const params = {
|
const params = {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
@@ -20,7 +48,7 @@ async function login(loginUrl, username, password, controllerVersionMajor) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const [status, contentType, data] = await httpProxy(loginUrl, {
|
const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(params),
|
body: JSON.stringify(params),
|
||||||
headers: {
|
headers: {
|
||||||
@@ -28,7 +56,20 @@ async function login(loginUrl, username, password, controllerVersionMajor) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return [status, JSON.parse(data.toString())];
|
const loginResponseData = JSON.parse(data.toString());
|
||||||
|
|
||||||
|
if (status === 200 && loginResponseData.errorCode === 0) {
|
||||||
|
cache.put(
|
||||||
|
sessionCacheId,
|
||||||
|
{
|
||||||
|
token: loginResponseData.result.token,
|
||||||
|
cookieHeader: getCookieHeader(responseHeaders),
|
||||||
|
},
|
||||||
|
55 * 60 * 1000, // Cache session for 55 minutes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [status, loginResponseData];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function omadaProxyHandler(req, res) {
|
export default async function omadaProxyHandler(req, res) {
|
||||||
@@ -86,182 +127,247 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [loginStatus, loginResponseData] = await login(
|
const sessionCacheId = getSessionCacheId(group, service, index);
|
||||||
loginUrl,
|
let session = cache.get(sessionCacheId);
|
||||||
widget.username,
|
|
||||||
widget.password,
|
|
||||||
controllerVersionMajor,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||||
return res
|
try {
|
||||||
.status(loginStatus)
|
const usedCachedSession = Boolean(session);
|
||||||
.json({ error: { message: "Error logging in to Omada controller", url: loginUrl, data: loginResponseData } });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { token } = loginResponseData.result;
|
if (!session) {
|
||||||
|
const [loginStatus, loginResponseData] = await login(
|
||||||
|
loginUrl,
|
||||||
|
widget.username,
|
||||||
|
widget.password,
|
||||||
|
controllerVersionMajor,
|
||||||
|
sessionCacheId,
|
||||||
|
);
|
||||||
|
|
||||||
let sitesUrl;
|
if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
|
||||||
let body = {};
|
return res.status(loginStatus).json({
|
||||||
let params = { token };
|
error: { message: "Error logging in to Omada controller", url: loginUrl, data: loginResponseData },
|
||||||
let headers = { "Csrf-Token": token };
|
});
|
||||||
let method = "GET";
|
}
|
||||||
|
|
||||||
switch (controllerVersionMajor) {
|
session = cache.get(sessionCacheId);
|
||||||
case 3:
|
}
|
||||||
sitesUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
|
|
||||||
body = {
|
|
||||||
method: "getUserSites",
|
|
||||||
params: {
|
|
||||||
userName: widget.username,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
method = "POST";
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
sitesUrl = `${url}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`;
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
case 6:
|
|
||||||
sitesUrl = `${url}/${cId}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
[status, contentType, data] = await httpProxy(sitesUrl, {
|
const { token, cookieHeader } = session;
|
||||||
method,
|
|
||||||
params,
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sitesResponseData = JSON.parse(data);
|
let sitesUrl;
|
||||||
|
let body = {};
|
||||||
|
let params = { token };
|
||||||
|
let headers = { "Csrf-Token": token };
|
||||||
|
if (cookieHeader) {
|
||||||
|
headers.Cookie = cookieHeader;
|
||||||
|
}
|
||||||
|
let method = "GET";
|
||||||
|
|
||||||
if (status !== 200 || sitesResponseData.errorCode > 0) {
|
switch (controllerVersionMajor) {
|
||||||
logger.debug(`HTTP ${status} getting sites list: ${sitesResponseData.msg}`);
|
case 3:
|
||||||
return res
|
sitesUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
|
||||||
.status(status)
|
body = {
|
||||||
.json({ error: { message: "Error getting sites list", url, data: sitesResponseData } });
|
method: "getUserSites",
|
||||||
}
|
params: {
|
||||||
|
userName: widget.username,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
headers = { "Content-Type": "application/json" };
|
||||||
|
if (cookieHeader) {
|
||||||
|
headers.Cookie = cookieHeader;
|
||||||
|
}
|
||||||
|
method = "POST";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
sitesUrl = `${url}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`;
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
case 6:
|
||||||
|
sitesUrl = `${url}/${cId}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const site =
|
[status, contentType, data] = await httpProxy(sitesUrl, {
|
||||||
controllerVersionMajor === 3
|
method,
|
||||||
? sitesResponseData.result.siteList.find((s) => s.name === widget.site)
|
params,
|
||||||
: sitesResponseData.result.data.find((s) => s.name === widget.site);
|
body: JSON.stringify(body),
|
||||||
|
headers: { ...headers },
|
||||||
if (!site) {
|
|
||||||
return res.status(status).json({ error: { message: `Site ${widget.site} is not found`, url: sitesUrl, data } });
|
|
||||||
}
|
|
||||||
|
|
||||||
let siteResponseData;
|
|
||||||
|
|
||||||
let connectedAp;
|
|
||||||
let activeUser;
|
|
||||||
let connectedSwitches;
|
|
||||||
let connectedGateways;
|
|
||||||
let alerts;
|
|
||||||
|
|
||||||
if (controllerVersionMajor === 3) {
|
|
||||||
// Omada v3 controller requires switching site
|
|
||||||
const switchUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
|
|
||||||
method = "POST";
|
|
||||||
body = {
|
|
||||||
method: "switchSite",
|
|
||||||
params: {
|
|
||||||
siteName: site.siteName,
|
|
||||||
userName: widget.username,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
headers = { "Content-Type": "application/json" };
|
|
||||||
params = { token };
|
|
||||||
|
|
||||||
[status, contentType, data] = await httpProxy(switchUrl, {
|
|
||||||
method,
|
|
||||||
params,
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
const switchResponseData = JSON.parse(data);
|
|
||||||
if (status !== 200 || switchResponseData.errorCode > 0) {
|
|
||||||
logger.error(`HTTP ${status} getting sites list: ${data}`);
|
|
||||||
return res.status(status).json({ error: { message: "Error switching site", url: switchUrl, data } });
|
|
||||||
}
|
|
||||||
|
|
||||||
const statsUrl = `${url}/web/v1/controller?getGlobalStat=&token=${token}`;
|
|
||||||
[status, contentType, data] = await httpProxy(statsUrl, {
|
|
||||||
method,
|
|
||||||
params,
|
|
||||||
body: JSON.stringify({
|
|
||||||
method: "getGlobalStat",
|
|
||||||
}),
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
siteResponseData = JSON.parse(data);
|
|
||||||
|
|
||||||
if (status !== 200 || siteResponseData.errorCode > 0) {
|
|
||||||
return res.status(status).json({ error: { message: "Error getting stats", url: statsUrl, data } });
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedAp = siteResponseData.result.connectedAp;
|
|
||||||
activeUser = siteResponseData.result.activeUser;
|
|
||||||
alerts = siteResponseData.result.alerts;
|
|
||||||
} else if ([4, 5, 6].includes(controllerVersionMajor)) {
|
|
||||||
const siteName = controllerVersionMajor > 4 ? site.id : site.key;
|
|
||||||
const siteStatsUrl =
|
|
||||||
controllerVersionMajor === 4
|
|
||||||
? `${url}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}¤tPage=1¤tPageSize=1000`
|
|
||||||
: `${url}/${cId}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}¤tPage=1¤tPageSize=1000`;
|
|
||||||
|
|
||||||
[status, contentType, data] = await httpProxy(siteStatsUrl, {
|
|
||||||
headers: {
|
|
||||||
"Csrf-Token": token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
siteResponseData = JSON.parse(data);
|
|
||||||
|
|
||||||
if (status !== 200 || siteResponseData.errorCode > 0) {
|
|
||||||
logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`);
|
|
||||||
return res.status(status === 200 ? 500 : status).json({
|
|
||||||
error: {
|
|
||||||
message: "Error getting stats",
|
|
||||||
url: siteStatsUrl,
|
|
||||||
data: siteResponseData,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sitesResponseData = JSON.parse(data);
|
||||||
|
|
||||||
|
if (status !== 200 || sitesResponseData.errorCode > 0) {
|
||||||
|
logger.debug(`HTTP ${status} getting sites list: ${sitesResponseData.msg}`);
|
||||||
|
if (shouldRetryWithFreshSession(status, sitesResponseData, attempt, usedCachedSession)) {
|
||||||
|
cache.del(sessionCacheId);
|
||||||
|
session = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
.status(status)
|
||||||
|
.json({ error: { message: "Error getting sites list", url, data: sitesResponseData } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const site =
|
||||||
|
controllerVersionMajor === 3
|
||||||
|
? sitesResponseData.result.siteList.find((s) => s.name === widget.site)
|
||||||
|
: sitesResponseData.result.data.find((s) => s.name === widget.site);
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
return res
|
||||||
|
.status(status)
|
||||||
|
.json({ error: { message: `Site ${widget.site} is not found`, url: sitesUrl, data } });
|
||||||
|
}
|
||||||
|
|
||||||
|
let siteResponseData;
|
||||||
|
|
||||||
|
let connectedAp;
|
||||||
|
let activeUser;
|
||||||
|
let connectedSwitches;
|
||||||
|
let connectedGateways;
|
||||||
|
let alerts;
|
||||||
|
|
||||||
|
if (controllerVersionMajor === 3) {
|
||||||
|
// Omada v3 controller requires switching site
|
||||||
|
const switchUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
|
||||||
|
method = "POST";
|
||||||
|
body = {
|
||||||
|
method: "switchSite",
|
||||||
|
params: {
|
||||||
|
siteName: site.siteName,
|
||||||
|
userName: widget.username,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
headers = { "Content-Type": "application/json" };
|
||||||
|
if (cookieHeader) {
|
||||||
|
headers.Cookie = cookieHeader;
|
||||||
|
}
|
||||||
|
params = { token };
|
||||||
|
|
||||||
|
[status, contentType, data] = await httpProxy(switchUrl, {
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { ...headers },
|
||||||
|
});
|
||||||
|
|
||||||
|
const switchResponseData = JSON.parse(data);
|
||||||
|
if (status !== 200 || switchResponseData.errorCode > 0) {
|
||||||
|
logger.error(`HTTP ${status} getting sites list: ${data}`);
|
||||||
|
if (shouldRetryWithFreshSession(status, switchResponseData, attempt, usedCachedSession)) {
|
||||||
|
cache.del(sessionCacheId);
|
||||||
|
session = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return res.status(status).json({ error: { message: "Error switching site", url: switchUrl, data } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const statsUrl = `${url}/web/v1/controller?getGlobalStat=&token=${token}`;
|
||||||
|
[status, contentType, data] = await httpProxy(statsUrl, {
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
body: JSON.stringify({
|
||||||
|
method: "getGlobalStat",
|
||||||
|
}),
|
||||||
|
headers: { ...headers },
|
||||||
|
});
|
||||||
|
|
||||||
|
siteResponseData = JSON.parse(data);
|
||||||
|
|
||||||
|
if (status !== 200 || siteResponseData.errorCode > 0) {
|
||||||
|
if (shouldRetryWithFreshSession(status, siteResponseData, attempt, usedCachedSession)) {
|
||||||
|
cache.del(sessionCacheId);
|
||||||
|
session = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return res.status(status).json({ error: { message: "Error getting stats", url: statsUrl, data } });
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedAp = siteResponseData.result.connectedAp;
|
||||||
|
activeUser = siteResponseData.result.activeUser;
|
||||||
|
alerts = siteResponseData.result.alerts;
|
||||||
|
} else if ([4, 5, 6].includes(controllerVersionMajor)) {
|
||||||
|
const siteName = controllerVersionMajor > 4 ? site.id : site.key;
|
||||||
|
const siteStatsUrl =
|
||||||
|
controllerVersionMajor === 4
|
||||||
|
? `${url}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}¤tPage=1¤tPageSize=1000`
|
||||||
|
: `${url}/${cId}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}¤tPage=1¤tPageSize=1000`;
|
||||||
|
|
||||||
|
[status, contentType, data] = await httpProxy(siteStatsUrl, {
|
||||||
|
headers: { ...headers },
|
||||||
|
});
|
||||||
|
|
||||||
|
siteResponseData = JSON.parse(data);
|
||||||
|
|
||||||
|
if (status !== 200 || siteResponseData.errorCode > 0) {
|
||||||
|
logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`);
|
||||||
|
if (shouldRetryWithFreshSession(status, siteResponseData, attempt, usedCachedSession)) {
|
||||||
|
cache.del(sessionCacheId);
|
||||||
|
session = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return res.status(status === 200 ? 500 : status).json({
|
||||||
|
error: {
|
||||||
|
message: "Error getting stats",
|
||||||
|
url: siteStatsUrl,
|
||||||
|
data: siteResponseData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertUrl =
|
||||||
|
controllerVersionMajor === 4
|
||||||
|
? `${url}/api/v2/sites/${siteName}/alerts/num?token=${token}¤tPage=1¤tPageSize=1000`
|
||||||
|
: `${url}/${cId}/api/v2/sites/${siteName}/alerts/num?token=${token}¤tPage=1¤tPageSize=1000`;
|
||||||
|
|
||||||
|
[status, contentType, data] = await httpProxy(alertUrl, {
|
||||||
|
headers: { ...headers },
|
||||||
|
});
|
||||||
|
const alertResponseData = JSON.parse(data);
|
||||||
|
|
||||||
|
if (status !== 200 || alertResponseData.errorCode > 0) {
|
||||||
|
if (shouldRetryWithFreshSession(status, alertResponseData, attempt, usedCachedSession)) {
|
||||||
|
cache.del(sessionCacheId);
|
||||||
|
session = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return res.status(status === 200 ? 500 : status).json({
|
||||||
|
error: {
|
||||||
|
message: "Error getting alerts",
|
||||||
|
url: alertUrl,
|
||||||
|
data: alertResponseData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
activeUser = siteResponseData.result.totalClientNum;
|
||||||
|
connectedAp = siteResponseData.result.connectedApNum;
|
||||||
|
connectedGateways = siteResponseData.result.connectedGatewayNum;
|
||||||
|
connectedSwitches = siteResponseData.result.connectedSwitchNum;
|
||||||
|
alerts = alertResponseData.result.alertNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send(
|
||||||
|
JSON.stringify({
|
||||||
|
connectedAp,
|
||||||
|
activeUser,
|
||||||
|
alerts,
|
||||||
|
connectedGateways,
|
||||||
|
connectedSwitches,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof SyntaxError && attempt === 0) {
|
||||||
|
cache.del(sessionCacheId);
|
||||||
|
session = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const alertUrl =
|
|
||||||
controllerVersionMajor === 4
|
|
||||||
? `${url}/api/v2/sites/${siteName}/alerts/num?token=${token}¤tPage=1¤tPageSize=1000`
|
|
||||||
: `${url}/${cId}/api/v2/sites/${siteName}/alerts/num?token=${token}¤tPage=1¤tPageSize=1000`;
|
|
||||||
|
|
||||||
[status, contentType, data] = await httpProxy(alertUrl, {
|
|
||||||
headers: {
|
|
||||||
"Csrf-Token": token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const alertResponseData = JSON.parse(data);
|
|
||||||
|
|
||||||
activeUser = siteResponseData.result.totalClientNum;
|
|
||||||
connectedAp = siteResponseData.result.connectedApNum;
|
|
||||||
connectedGateways = siteResponseData.result.connectedGatewayNum;
|
|
||||||
connectedSwitches = siteResponseData.result.connectedSwitchNum;
|
|
||||||
alerts = alertResponseData.result.alertNum;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.send(
|
|
||||||
JSON.stringify({
|
|
||||||
connectedAp,
|
|
||||||
activeUser,
|
|
||||||
alerts,
|
|
||||||
connectedGateways,
|
|
||||||
connectedSwitches,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,24 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
import createMockRes from "test-utils/create-mock-res";
|
import createMockRes from "test-utils/create-mock-res";
|
||||||
|
|
||||||
const { httpProxy, getServiceWidget, logger } = vi.hoisted(() => ({
|
const { httpProxy, getServiceWidget, cache, logger } = vi.hoisted(() => {
|
||||||
httpProxy: vi.fn(),
|
const store = new Map();
|
||||||
getServiceWidget: vi.fn(),
|
|
||||||
logger: {
|
return {
|
||||||
debug: vi.fn(),
|
httpProxy: vi.fn(),
|
||||||
error: vi.fn(),
|
getServiceWidget: vi.fn(),
|
||||||
},
|
cache: {
|
||||||
}));
|
get: vi.fn((k) => store.get(k)),
|
||||||
|
put: vi.fn((k, v) => store.set(k, v)),
|
||||||
|
del: vi.fn((k) => store.delete(k)),
|
||||||
|
_reset: () => store.clear(),
|
||||||
|
},
|
||||||
|
logger: {
|
||||||
|
debug: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("utils/logger", () => ({
|
vi.mock("utils/logger", () => ({
|
||||||
default: () => logger,
|
default: () => logger,
|
||||||
@@ -20,15 +30,19 @@ vi.mock("utils/config/service-helpers", () => ({
|
|||||||
vi.mock("utils/proxy/http", () => ({
|
vi.mock("utils/proxy/http", () => ({
|
||||||
httpProxy,
|
httpProxy,
|
||||||
}));
|
}));
|
||||||
|
vi.mock("memory-cache", () => ({
|
||||||
|
default: cache,
|
||||||
|
...cache,
|
||||||
|
}));
|
||||||
|
|
||||||
import omadaProxyHandler from "./proxy";
|
import omadaProxyHandler from "./proxy";
|
||||||
|
|
||||||
describe("widgets/omada/proxy", () => {
|
describe("widgets/omada/proxy", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// Clear one-off implementations between tests (some branches return early).
|
|
||||||
httpProxy.mockReset();
|
httpProxy.mockReset();
|
||||||
getServiceWidget.mockReset();
|
getServiceWidget.mockReset();
|
||||||
|
cache._reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fetches controller info, logs in, selects site, and returns overview stats (v4)", async () => {
|
it("fetches controller info, logs in, selects site, and returns overview stats (v4)", async () => {
|
||||||
@@ -51,6 +65,7 @@ describe("widgets/omada/proxy", () => {
|
|||||||
200,
|
200,
|
||||||
"application/json",
|
"application/json",
|
||||||
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
|
||||||
])
|
])
|
||||||
// sites list
|
// sites list
|
||||||
.mockResolvedValueOnce([
|
.mockResolvedValueOnce([
|
||||||
@@ -91,6 +106,12 @@ describe("widgets/omada/proxy", () => {
|
|||||||
connectedSwitches: 3,
|
connectedSwitches: 3,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
expect(httpProxy.mock.calls[2][1]).toMatchObject({
|
||||||
|
headers: {
|
||||||
|
"Csrf-Token": "t",
|
||||||
|
Cookie: "TPOMADA_SESSIONID=sid",
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an error when controller info cannot be retrieved", async () => {
|
it("returns an error when controller info cannot be retrieved", async () => {
|
||||||
@@ -169,6 +190,7 @@ describe("widgets/omada/proxy", () => {
|
|||||||
200,
|
200,
|
||||||
"application/json",
|
"application/json",
|
||||||
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
|
||||||
])
|
])
|
||||||
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 2, msg: "bad" })]);
|
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 2, msg: "bad" })]);
|
||||||
|
|
||||||
@@ -195,6 +217,7 @@ describe("widgets/omada/proxy", () => {
|
|||||||
200,
|
200,
|
||||||
"application/json",
|
"application/json",
|
||||||
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
|
||||||
])
|
])
|
||||||
.mockResolvedValueOnce([
|
.mockResolvedValueOnce([
|
||||||
200,
|
200,
|
||||||
@@ -222,6 +245,7 @@ describe("widgets/omada/proxy", () => {
|
|||||||
200,
|
200,
|
||||||
"application/json",
|
"application/json",
|
||||||
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
|
||||||
])
|
])
|
||||||
// getUserSites
|
// getUserSites
|
||||||
.mockResolvedValueOnce([
|
.mockResolvedValueOnce([
|
||||||
@@ -271,6 +295,7 @@ describe("widgets/omada/proxy", () => {
|
|||||||
200,
|
200,
|
||||||
"application/json",
|
"application/json",
|
||||||
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
|
||||||
])
|
])
|
||||||
.mockResolvedValueOnce([
|
.mockResolvedValueOnce([
|
||||||
200,
|
200,
|
||||||
@@ -301,6 +326,7 @@ describe("widgets/omada/proxy", () => {
|
|||||||
200,
|
200,
|
||||||
"application/json",
|
"application/json",
|
||||||
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
|
||||||
])
|
])
|
||||||
.mockResolvedValueOnce([
|
.mockResolvedValueOnce([
|
||||||
200,
|
200,
|
||||||
@@ -324,4 +350,414 @@ describe("widgets/omada/proxy", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reuses a cached Omada session across polls", async () => {
|
||||||
|
getServiceWidget.mockResolvedValue({
|
||||||
|
url: "http://omada",
|
||||||
|
username: "u",
|
||||||
|
password: "p",
|
||||||
|
site: "Default",
|
||||||
|
});
|
||||||
|
|
||||||
|
httpProxy
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({
|
||||||
|
errorCode: 0,
|
||||||
|
result: {
|
||||||
|
totalClientNum: 10,
|
||||||
|
connectedApNum: 2,
|
||||||
|
connectedGatewayNum: 1,
|
||||||
|
connectedSwitchNum: 3,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({
|
||||||
|
errorCode: 0,
|
||||||
|
result: {
|
||||||
|
totalClientNum: 10,
|
||||||
|
connectedApNum: 2,
|
||||||
|
connectedGatewayNum: 1,
|
||||||
|
connectedSwitchNum: 3,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })]);
|
||||||
|
|
||||||
|
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||||
|
|
||||||
|
await omadaProxyHandler(req, createMockRes());
|
||||||
|
await omadaProxyHandler(req, createMockRes());
|
||||||
|
|
||||||
|
const loginCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("/api/v2/login"));
|
||||||
|
expect(loginCalls).toHaveLength(1);
|
||||||
|
expect(httpProxy.mock.calls[6][1].headers.Cookie).toBe("TPOMADA_SESSIONID=sid");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not reuse a cached session across different widget identities", async () => {
|
||||||
|
getServiceWidget.mockResolvedValue({
|
||||||
|
url: "http://omada",
|
||||||
|
username: "u",
|
||||||
|
password: "p",
|
||||||
|
site: "Default",
|
||||||
|
});
|
||||||
|
|
||||||
|
httpProxy
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t1" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=sid1; Path=/; HttpOnly"] },
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({
|
||||||
|
errorCode: 0,
|
||||||
|
result: {
|
||||||
|
totalClientNum: 10,
|
||||||
|
connectedApNum: 2,
|
||||||
|
connectedGatewayNum: 1,
|
||||||
|
connectedSwitchNum: 3,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t2" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=sid2; Path=/; HttpOnly"] },
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({
|
||||||
|
errorCode: 0,
|
||||||
|
result: {
|
||||||
|
totalClientNum: 10,
|
||||||
|
connectedApNum: 2,
|
||||||
|
connectedGatewayNum: 1,
|
||||||
|
connectedSwitchNum: 3,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })]);
|
||||||
|
|
||||||
|
await omadaProxyHandler({ query: { group: "g1", service: "svc", index: "0" } }, createMockRes());
|
||||||
|
await omadaProxyHandler({ query: { group: "g2", service: "svc", index: "0" } }, createMockRes());
|
||||||
|
|
||||||
|
const loginCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("/api/v2/login"));
|
||||||
|
expect(loginCalls).toHaveLength(2);
|
||||||
|
expect(httpProxy.mock.calls[2][1].headers.Cookie).toBe("TPOMADA_SESSIONID=sid1");
|
||||||
|
expect(httpProxy.mock.calls[7][1].headers.Cookie).toBe("TPOMADA_SESSIONID=sid2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the latest value when Omada sets the same cookie more than once during login", async () => {
|
||||||
|
getServiceWidget.mockResolvedValue({
|
||||||
|
url: "http://omada",
|
||||||
|
username: "u",
|
||||||
|
password: "p",
|
||||||
|
site: "Default",
|
||||||
|
});
|
||||||
|
|
||||||
|
httpProxy
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
||||||
|
{
|
||||||
|
"set-cookie": [
|
||||||
|
"TPOMADA_SESSIONID=deleteMe; Path=/; Max-Age=0",
|
||||||
|
"TPOMADA_SESSIONID=sid; Path=/; HttpOnly",
|
||||||
|
"rememberMe=deleteMe; Path=/; Max-Age=0",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({
|
||||||
|
errorCode: 0,
|
||||||
|
result: {
|
||||||
|
totalClientNum: 10,
|
||||||
|
connectedApNum: 2,
|
||||||
|
connectedGatewayNum: 1,
|
||||||
|
connectedSwitchNum: 3,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })]);
|
||||||
|
|
||||||
|
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await omadaProxyHandler(req, res);
|
||||||
|
|
||||||
|
expect(httpProxy.mock.calls[2][1].headers.Cookie).toBe("TPOMADA_SESSIONID=sid; rememberMe=deleteMe");
|
||||||
|
expect(res.body).toBe(
|
||||||
|
JSON.stringify({
|
||||||
|
connectedAp: 2,
|
||||||
|
activeUser: 10,
|
||||||
|
alerts: 4,
|
||||||
|
connectedGateways: 1,
|
||||||
|
connectedSwitches: 3,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not reuse a mutated content-length header on later GET requests", async () => {
|
||||||
|
getServiceWidget.mockResolvedValue({
|
||||||
|
url: "http://omada",
|
||||||
|
username: "u",
|
||||||
|
password: "p",
|
||||||
|
site: "Default",
|
||||||
|
});
|
||||||
|
|
||||||
|
const responses = [
|
||||||
|
[200, "application/json", JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } })],
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({
|
||||||
|
errorCode: 0,
|
||||||
|
result: {
|
||||||
|
totalClientNum: 10,
|
||||||
|
connectedApNum: 2,
|
||||||
|
connectedGatewayNum: 1,
|
||||||
|
connectedSwitchNum: 3,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })],
|
||||||
|
];
|
||||||
|
|
||||||
|
httpProxy.mockImplementation(async (_url, params = {}) => {
|
||||||
|
if (params.body) {
|
||||||
|
params.headers["content-length"] = Buffer.byteLength(params.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses.shift();
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await omadaProxyHandler(req, res);
|
||||||
|
|
||||||
|
expect(httpProxy.mock.calls[2][1].headers["content-length"]).toBe(2);
|
||||||
|
expect(httpProxy.mock.calls[3][1].headers["content-length"]).toBeUndefined();
|
||||||
|
expect(httpProxy.mock.calls[4][1].headers["content-length"]).toBeUndefined();
|
||||||
|
expect(res.body).toBe(
|
||||||
|
JSON.stringify({
|
||||||
|
connectedAp: 2,
|
||||||
|
activeUser: 10,
|
||||||
|
alerts: 4,
|
||||||
|
connectedGateways: 1,
|
||||||
|
connectedSwitches: 3,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the cached session and re-authenticates when an authenticated response is not JSON", async () => {
|
||||||
|
cache.put("omadaProxyHandler__session.g.svc.0", {
|
||||||
|
token: "stale-token",
|
||||||
|
cookieHeader: "TPOMADA_SESSIONID=stale",
|
||||||
|
});
|
||||||
|
|
||||||
|
getServiceWidget.mockResolvedValue({
|
||||||
|
url: "http://omada",
|
||||||
|
username: "u",
|
||||||
|
password: "p",
|
||||||
|
site: "Default",
|
||||||
|
});
|
||||||
|
|
||||||
|
httpProxy
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([200, "text/html", Buffer.from("<!DOCTYPE html>login")])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "fresh-token" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=fresh; Path=/; HttpOnly"] },
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({
|
||||||
|
errorCode: 0,
|
||||||
|
result: {
|
||||||
|
totalClientNum: 10,
|
||||||
|
connectedApNum: 2,
|
||||||
|
connectedGatewayNum: 1,
|
||||||
|
connectedSwitchNum: 3,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })]);
|
||||||
|
|
||||||
|
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await omadaProxyHandler(req, res);
|
||||||
|
|
||||||
|
expect(cache.del).toHaveBeenCalledWith("omadaProxyHandler__session.g.svc.0");
|
||||||
|
const loginCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("/api/v2/login"));
|
||||||
|
expect(loginCalls).toHaveLength(1);
|
||||||
|
expect(res.body).toBe(
|
||||||
|
JSON.stringify({
|
||||||
|
connectedAp: 2,
|
||||||
|
activeUser: 10,
|
||||||
|
alerts: 4,
|
||||||
|
connectedGateways: 1,
|
||||||
|
connectedSwitches: 3,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the cached session and re-authenticates when a cached session returns a JSON auth error", async () => {
|
||||||
|
cache.put("omadaProxyHandler__session.g.svc.0", {
|
||||||
|
token: "stale-token",
|
||||||
|
cookieHeader: "TPOMADA_SESSIONID=stale",
|
||||||
|
});
|
||||||
|
|
||||||
|
getServiceWidget.mockResolvedValue({
|
||||||
|
url: "http://omada",
|
||||||
|
username: "u",
|
||||||
|
password: "p",
|
||||||
|
site: "Default",
|
||||||
|
});
|
||||||
|
|
||||||
|
httpProxy
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 1, msg: "Login required" })])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "fresh-token" } })),
|
||||||
|
{ "set-cookie": ["TPOMADA_SESSIONID=fresh; Path=/; HttpOnly"] },
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
200,
|
||||||
|
"application/json",
|
||||||
|
JSON.stringify({
|
||||||
|
errorCode: 0,
|
||||||
|
result: {
|
||||||
|
totalClientNum: 10,
|
||||||
|
connectedApNum: 2,
|
||||||
|
connectedGatewayNum: 1,
|
||||||
|
connectedSwitchNum: 3,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })]);
|
||||||
|
|
||||||
|
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await omadaProxyHandler(req, res);
|
||||||
|
|
||||||
|
expect(cache.del).toHaveBeenCalledWith("omadaProxyHandler__session.g.svc.0");
|
||||||
|
const loginCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("/api/v2/login"));
|
||||||
|
expect(loginCalls).toHaveLength(1);
|
||||||
|
expect(res.body).toBe(
|
||||||
|
JSON.stringify({
|
||||||
|
connectedAp: 2,
|
||||||
|
activeUser: 10,
|
||||||
|
alerts: 4,
|
||||||
|
connectedGateways: 1,
|
||||||
|
connectedSwitches: 3,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user