feat: 使用 Playwright 实现自动化 cookie 获取和验证
主要变更: - 新增 Playwright 登录认证服务 (services/playwright-auth.ts) - 重构 get-activity.ts 使用 Playwright 替代 Axios 登录 - 实现自动 cookie 过期检测和重试机制 - 优化 Docker 配置支持 Playwright 浏览器运行 - 添加启动脚本自动验证和刷新 cookies - 完善错误处理:区分 4xx(认证失败) 和 5xx(服务器错误) 技术细节: - 删除旧版 login_template.txt 和 nkcs-engage.cookie.txt - 添加 startup.sh 启动时自动验证 cookies - 改进 cookie 验证逻辑,添加指数退避重试 - Dockerfile 安装 Playwright 系统依赖 - docker-compose.yaml 添加 volumes 和 health checks 测试: - 添加 auth.spec.ts 自动化测试 - 添加 get-cookies.ts 和 test-cookies-validity.ts 工具脚本 - 验证 401/500/000 等错误场景处理正确
This commit is contained in:
40
Dockerfile
40
Dockerfile
@@ -1,15 +1,53 @@
|
||||
FROM oven/bun:latest
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install Playwright system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libnss3 \
|
||||
libnspr4 \
|
||||
libatk1.0-0 \
|
||||
libatk-bridge2.0-0 \
|
||||
libcups2 \
|
||||
libdrm2 \
|
||||
libxkbcommon0 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxfixes3 \
|
||||
libxrandr2 \
|
||||
libgbm1 \
|
||||
libasound2 \
|
||||
libpango-1.0-0 \
|
||||
libcairo2 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy dependency files
|
||||
COPY package.json bun.lock ./
|
||||
|
||||
# Install dependencies (including Playwright)
|
||||
RUN bun install --production
|
||||
|
||||
# Install Playwright browsers
|
||||
RUN bunx playwright install chromium --with-deps || true
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Make startup script executable
|
||||
RUN chmod +x startup.sh
|
||||
|
||||
# Create non-root user for security
|
||||
RUN adduser --disabled-password --gecos '' appuser && \
|
||||
chown -R appuser:appuser /usr/src/app
|
||||
|
||||
USER appuser
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["bun", "start"]
|
||||
# Use startup script
|
||||
CMD ["/bin/sh", "startup.sh"]
|
||||
|
||||
9
bun.lock
9
bun.lock
@@ -16,6 +16,7 @@
|
||||
"uuid": "^11.1.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@types/bun": "latest",
|
||||
"typescript-language-server": "^5.1.3",
|
||||
},
|
||||
@@ -67,6 +68,8 @@
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw=="],
|
||||
|
||||
"@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw=="],
|
||||
@@ -149,6 +152,8 @@
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
@@ -205,6 +210,10 @@
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
|
||||
|
||||
"playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
@@ -5,21 +5,29 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
container_name: dsas-cca-backend
|
||||
ports:
|
||||
- "${PORT}:${PORT}"
|
||||
- "${PORT:-3000}:${PORT:-3000}"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- redis
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./services/cookies.json:/usr/src/app/services/cookies.json
|
||||
networks:
|
||||
- cca_network
|
||||
mem_limit: 1g
|
||||
cpus: 1.0
|
||||
|
||||
redis:
|
||||
image: "redis:8.0-alpine"
|
||||
container_name: dsas-cca-redis
|
||||
command: redis-server --requirepass "dsas-cca"
|
||||
volumes:
|
||||
- ./redis_data:/data
|
||||
- redis_data:/data
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- cca_network
|
||||
@@ -28,6 +36,10 @@ services:
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
mem_limit: 256m
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
cca_network:
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
// engage-api/get-activity.ts
|
||||
import axios from 'axios';
|
||||
import { readFile,writeFile,unlink } from 'fs/promises';
|
||||
import { resolve } from 'path';
|
||||
import { logger } from '../utils/logger';
|
||||
import {
|
||||
loginWithPlaywright,
|
||||
loadCachedCookies,
|
||||
saveCookiesToCache,
|
||||
clearCookieCache,
|
||||
getCachedCookieString
|
||||
} from '../services/playwright-auth';
|
||||
|
||||
// Define interfaces for our data structures
|
||||
interface ActivityResponse {
|
||||
d: string;
|
||||
isError ? : boolean;
|
||||
isError?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@@ -15,71 +20,19 @@ interface ActivityResponse {
|
||||
class AuthenticationError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(message: string = "Authentication failed, cookie may be invalid.", status ? : number) {
|
||||
constructor(message: string = "Authentication failed, cookie may be invalid.", status?: number) {
|
||||
super(message);
|
||||
this.name = "AuthenticationError";
|
||||
this.status = status || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// In Bun, we can use import.meta.dir instead of the Node.js __dirname approach
|
||||
const COOKIE_FILE_PATH = resolve(import.meta.dir, 'nkcs-engage.cookie.txt');
|
||||
let _inMemoryCookie: string | null = null;
|
||||
|
||||
// Cookie Cache Helper Functions
|
||||
async function loadCachedCookie(): Promise < string | null > {
|
||||
if (_inMemoryCookie) {
|
||||
logger.debug("Using in-memory cached cookie.");
|
||||
return _inMemoryCookie;
|
||||
}
|
||||
try {
|
||||
const cookieFromFile = await readFile(COOKIE_FILE_PATH, 'utf8');
|
||||
if (cookieFromFile) {
|
||||
_inMemoryCookie = cookieFromFile;
|
||||
logger.debug("Loaded cookie from file cache.");
|
||||
return _inMemoryCookie;
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.code === 'ENOENT') {
|
||||
logger.debug("Cookie cache file not found. No cached cookie loaded.");
|
||||
} else {
|
||||
logger.warn("Error loading cookie from file:", err.message);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function saveCookieToCache(cookieString: string): Promise < void > {
|
||||
if (!cookieString) {
|
||||
logger.warn("Attempted to save an empty or null cookie. Aborting save.");
|
||||
return;
|
||||
}
|
||||
_inMemoryCookie = cookieString;
|
||||
try {
|
||||
await writeFile(COOKIE_FILE_PATH, cookieString, 'utf8');
|
||||
logger.debug("Cookie saved to file cache.");
|
||||
} catch (err: any) {
|
||||
logger.error("Error saving cookie to file:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearCookieCache(): Promise < void > {
|
||||
_inMemoryCookie = null;
|
||||
try {
|
||||
await unlink(COOKIE_FILE_PATH);
|
||||
logger.debug("Cookie cache file deleted.");
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
logger.error("Error deleting cookie file:", err.message);
|
||||
} else {
|
||||
logger.debug("Cookie cache file did not exist, no need to delete.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function testCookieValidity(cookieString: string): Promise < boolean > {
|
||||
/**
|
||||
* Test cookie validity by calling API
|
||||
*/
|
||||
async function testCookieValidityWithApi(cookieString: string): Promise<boolean> {
|
||||
if (!cookieString) return false;
|
||||
logger.debug("Testing cookie validity...");
|
||||
logger.debug('Testing cookie validity via API...');
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
let attempt = 0;
|
||||
@@ -98,123 +51,69 @@ async function testCookieValidity(cookieString: string): Promise < boolean > {
|
||||
};
|
||||
|
||||
logger.debug(`Attempt ${attempt}/${MAX_RETRIES}`);
|
||||
await axios.post(url, payload, {
|
||||
const response = await axios.post(url, payload, {
|
||||
headers,
|
||||
timeout: 20000
|
||||
});
|
||||
|
||||
logger.debug("Cookie test successful (API responded 2xx). Cookie is valid.");
|
||||
// Check for 4xx errors (auth failures)
|
||||
if (response.status >= 400 && response.status < 500) {
|
||||
logger.warn(`Cookie test returned ${response.status}, likely invalid`);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug('Cookie test successful (API responded 2xx). Cookie is valid.');
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
logger.warn(`Cookie validity test failed (attempt ${attempt}/${MAX_RETRIES}).`);
|
||||
if (error.response) {
|
||||
logger.warn(`Cookie test API response status: ${error.response.status}.`);
|
||||
} else {
|
||||
logger.warn(`Network/other error: ${error.message}`);
|
||||
}
|
||||
|
||||
if (attempt >= MAX_RETRIES) {
|
||||
logger.warn("Max retries reached. Cookie is likely invalid or expired.");
|
||||
// 4xx = auth failure (immediate fail)
|
||||
if (error.response.status >= 400 && error.response.status < 500) {
|
||||
logger.warn(`Cookie test API response status: ${error.response.status} (auth error)`);
|
||||
return false;
|
||||
}
|
||||
// 5xx = server error (retry with delay)
|
||||
logger.warn(`Cookie test API response status: ${error.response.status} (server error, retrying...)`);
|
||||
} else {
|
||||
// No response (000 status, network error, timeout)
|
||||
logger.warn(`Network/timeout error: ${error.message} (retrying...)`);
|
||||
}
|
||||
|
||||
if (attempt < MAX_RETRIES) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn('Max retries reached. Cookie is likely invalid or expired.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Core API Interaction Functions
|
||||
async function getSessionId(): Promise < string | null > {
|
||||
const url = 'https://engage.nkcswx.cn/Login.aspx';
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Bun DSAS-CCA get-activity Module)'
|
||||
}
|
||||
});
|
||||
const setCookieHeader = response.headers['set-cookie'];
|
||||
if (setCookieHeader && setCookieHeader.length > 0) {
|
||||
const sessionIdCookie = setCookieHeader.find(cookie => cookie.trim().startsWith('ASP.NET_SessionId='));
|
||||
if (sessionIdCookie) {
|
||||
logger.debug('ASP.NET_SessionId created');
|
||||
return sessionIdCookie.split(';')[0] || null; // Ensure a fallback to `null` if splitting fails
|
||||
}
|
||||
return null; // Explicitly return `null` if no cookie is found
|
||||
}
|
||||
logger.error("No ASP.NET_SessionId cookie found in Set-Cookie header.");
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
logger.error(`Error in getSessionId: ${error.response ? `${error.response.status} - ${error.response.statusText}` : error.message}`);
|
||||
throw error;
|
||||
/**
|
||||
* Get complete cookies using Playwright
|
||||
*/
|
||||
async function getCompleteCookies(userName: string, userPwd: string): Promise<string> {
|
||||
logger.info('Attempting to get complete cookie string using Playwright login...');
|
||||
|
||||
const cookies = await loginWithPlaywright(userName, userPwd);
|
||||
|
||||
if (!cookies || cookies.length === 0) {
|
||||
throw new Error("Login failed: Could not obtain cookies.");
|
||||
}
|
||||
|
||||
const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||
return cookieString;
|
||||
}
|
||||
|
||||
async function getMSAUTH(sessionId: string, userName: string, userPwd: string, templateFilePath: string): Promise < string | null > {
|
||||
const url = 'https://engage.nkcswx.cn/Login.aspx';
|
||||
try {
|
||||
let templateData = await readFile(templateFilePath, 'utf8');
|
||||
const postData = templateData
|
||||
.replace('{{USERNAME}}', userName)
|
||||
.replace('{{PASSWORD}}', userPwd);
|
||||
const headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Cookie': sessionId,
|
||||
'User-Agent': 'Mozilla/5.0 (Bun DSAS-CCA get-activity Module)',
|
||||
'Referer': 'https://engage.nkcswx.cn/Login.aspx'
|
||||
};
|
||||
logger.debug('Getting .ASPXFORMSAUTH');
|
||||
const response = await axios.post(url, postData, {
|
||||
headers,
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) => status >= 200 && status < 400
|
||||
});
|
||||
const setCookieHeader = response.headers['set-cookie'];
|
||||
let formsAuthCookieValue = null;
|
||||
if (setCookieHeader && setCookieHeader.length > 0) {
|
||||
const aspxAuthCookies = setCookieHeader.filter(cookie => cookie.trim().startsWith('.ASPXFORMSAUTH='));
|
||||
if (aspxAuthCookies.length > 0) {
|
||||
for (let i = aspxAuthCookies.length - 1; i >= 0; i--) {
|
||||
const cookieCandidateParts = aspxAuthCookies[i].split(';');
|
||||
if (cookieCandidateParts.length > 0 && cookieCandidateParts[0] !== undefined) { // Explicit check
|
||||
const firstPart = cookieCandidateParts[0].trim();
|
||||
if (firstPart.length > '.ASPXFORMSAUTH='.length && firstPart.substring('.ASPXFORMSAUTH='.length).length > 0) {
|
||||
formsAuthCookieValue = firstPart;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (formsAuthCookieValue) {
|
||||
logger.debug('.ASPXFORMSAUTH cookie obtained.');
|
||||
return formsAuthCookieValue;
|
||||
} else {
|
||||
logger.error("No valid .ASPXFORMSAUTH cookie found. Headers:", setCookieHeader || "none");
|
||||
return null;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') logger.error(`Error: Template file '${templateFilePath}' not found.`);
|
||||
else logger.error(`Error in getMSAUTH: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function getCompleteCookies(userName: string, userPwd: string, templateFilePath: string): Promise < string > {
|
||||
logger.debug('Attempting to get complete cookie string (login process).');
|
||||
const sessionId = await getSessionId();
|
||||
if (!sessionId) throw new Error("Login failed: Could not obtain ASP.NET_SessionId.");
|
||||
|
||||
const msAuth = await getMSAUTH(sessionId, userName, userPwd, templateFilePath);
|
||||
if (!msAuth) throw new Error("Login failed: Could not obtain .ASPXFORMSAUTH cookie.");
|
||||
|
||||
return `${sessionId}; ${msAuth}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get activity details from API
|
||||
*/
|
||||
async function getActivityDetailsRaw(
|
||||
activityId: string,
|
||||
cookies: string,
|
||||
maxRetries: number = 3,
|
||||
timeoutMilliseconds: number = 20000
|
||||
): Promise < string | null > {
|
||||
): Promise<string | null> {
|
||||
const url = 'https://engage.nkcswx.cn/Services/ActivitiesService.asmx/GetActivityDetails';
|
||||
const headers = {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
@@ -270,7 +169,6 @@ async function getActivityDetailsRaw(
|
||||
* @param activityId - The ID of the activity to fetch.
|
||||
* @param userName - URL-encoded username.
|
||||
* @param userPwd - URL-encoded password.
|
||||
* @param templateFileName - Name of the login template file.
|
||||
* @param forceLogin - If true, bypasses cached cookie and forces a new login.
|
||||
* @returns The parsed JSON object of activity details, or null on failure.
|
||||
*/
|
||||
@@ -278,10 +176,9 @@ export async function fetchActivityData(
|
||||
activityId: string,
|
||||
userName: string,
|
||||
userPwd: string,
|
||||
templateFileName: string = "login_template.txt",
|
||||
forceLogin: boolean = false
|
||||
): Promise < any | null > {
|
||||
let currentCookie = forceLogin ? null : await loadCachedCookie();
|
||||
): Promise<any | null> {
|
||||
let currentCookie = forceLogin ? null : await getCachedCookieString();
|
||||
|
||||
if (forceLogin && currentCookie) {
|
||||
await clearCookieCache();
|
||||
@@ -289,21 +186,25 @@ export async function fetchActivityData(
|
||||
}
|
||||
|
||||
if (currentCookie) {
|
||||
const isValid = await testCookieValidity(currentCookie);
|
||||
const isValid = await testCookieValidityWithApi(currentCookie);
|
||||
if (!isValid) {
|
||||
logger.info("Cached cookie test failed or cookie expired. Clearing cache.");
|
||||
logger.info('Cached cookie test failed or cookie expired. Clearing cache.');
|
||||
await clearCookieCache();
|
||||
currentCookie = null;
|
||||
} else {
|
||||
logger.info("Using valid cached cookie.");
|
||||
logger.info('Using valid cached cookie.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentCookie) {
|
||||
logger.info(forceLogin ? "Forcing new login." : "No valid cached cookie found or cache bypassed. Attempting login...");
|
||||
logger.info(forceLogin ? 'Forcing new login.' : 'No valid cached cookie found or cache bypassed. Attempting login...');
|
||||
try {
|
||||
currentCookie = await getCompleteCookies(userName, userPwd, resolve(import.meta.dir, templateFileName));
|
||||
await saveCookieToCache(currentCookie);
|
||||
currentCookie = await getCompleteCookies(userName, userPwd);
|
||||
|
||||
const cookies = await loadCachedCookies();
|
||||
if (cookies) {
|
||||
await saveCookiesToCache(cookies);
|
||||
}
|
||||
} catch (loginError) {
|
||||
logger.error(`Login process failed: ${(loginError as Error).message}`);
|
||||
return null;
|
||||
@@ -311,7 +212,7 @@ export async function fetchActivityData(
|
||||
}
|
||||
|
||||
if (!currentCookie) {
|
||||
logger.error("Critical: No cookie available after login attempt. Cannot fetch activity data.");
|
||||
logger.error('Critical: No cookie available after login attempt. Cannot fetch activity data.');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -329,11 +230,15 @@ export async function fetchActivityData(
|
||||
await clearCookieCache();
|
||||
|
||||
try {
|
||||
logger.info("Attempting re-login due to authentication failure...");
|
||||
currentCookie = await getCompleteCookies(userName, userPwd, resolve(import.meta.dir, templateFileName));
|
||||
await saveCookieToCache(currentCookie);
|
||||
logger.info('Attempting re-login due to authentication failure...');
|
||||
currentCookie = await getCompleteCookies(userName, userPwd);
|
||||
|
||||
logger.info("Re-login successful. Retrying request for activity details once...");
|
||||
const cookies = await loadCachedCookies();
|
||||
if (cookies) {
|
||||
await saveCookiesToCache(cookies);
|
||||
}
|
||||
|
||||
logger.info('Re-login successful. Retrying request for activity details once...');
|
||||
const rawActivityDetailsStringRetry = await getActivityDetailsRaw(activityId, currentCookie);
|
||||
if (rawActivityDetailsStringRetry) {
|
||||
const parsedOuterRetry = JSON.parse(rawActivityDetailsStringRetry);
|
||||
@@ -351,6 +256,3 @@ export async function fetchActivityData(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally
|
||||
//export { clearCookieCache,testCookieValidity };
|
||||
File diff suppressed because one or more lines are too long
@@ -1,112 +0,0 @@
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
async function extractLoginForm() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
|
||||
console.log('Navigating to login page...\n');
|
||||
await page.goto('https://engage.nkcswx.cn/Login.aspx');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Extract all form fields
|
||||
const formFields = await page.evaluate(() => {
|
||||
const form = document.querySelector('form');
|
||||
if (!form) return { error: 'No form found' };
|
||||
|
||||
const fields = [];
|
||||
|
||||
// Get all inputs from the form
|
||||
const inputs = form.querySelectorAll('input, select, textarea');
|
||||
inputs.forEach((input, index) => {
|
||||
fields.push({
|
||||
index,
|
||||
type: input.tagName,
|
||||
name: input.name || '(no name)',
|
||||
id: input.id || '(no id)',
|
||||
inputType: input.type || 'N/A',
|
||||
value: input.type === 'password' ? '[HIDDEN]' : (input.value || '(empty)'),
|
||||
placeholder: input.placeholder || '(none)',
|
||||
required: input.required ? 'yes' : 'no',
|
||||
autocomplete: input.autocomplete || '(none)'
|
||||
});
|
||||
});
|
||||
|
||||
// Get form attributes
|
||||
const formAttrs = {
|
||||
action: form.action,
|
||||
method: form.method,
|
||||
enctype: form.enctype,
|
||||
target: form.target
|
||||
};
|
||||
|
||||
return { formAttrs, fields };
|
||||
});
|
||||
|
||||
if (formFields.error) {
|
||||
console.error(formFields.error);
|
||||
await browser.close();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('='.repeat(70));
|
||||
console.log('FORM ATTRIBUTES');
|
||||
console.log('='.repeat(70));
|
||||
console.log(`Action: ${formFields.formAttrs.action}`);
|
||||
console.log(`Method: ${formFields.formAttrs.method}`);
|
||||
console.log(`Enctype: ${formFields.formAttrs.enctype}`);
|
||||
console.log(`Target: ${formFields.formAttrs.target || '(default)'}`);
|
||||
console.log('');
|
||||
|
||||
console.log('='.repeat(70));
|
||||
console.log('ALL FORM FIELDS');
|
||||
console.log('='.repeat(70));
|
||||
console.log('');
|
||||
|
||||
// Group fields by type
|
||||
const hiddenFields = formFields.fields.filter(f => f.inputType === 'hidden');
|
||||
const visibleFields = formFields.fields.filter(f => f.inputType !== 'hidden');
|
||||
|
||||
// Show hidden fields first (critical for ASP.NET)
|
||||
if (hiddenFields.length > 0) {
|
||||
console.log('📦 HIDDEN FIELDS (critical for form submission):');
|
||||
console.log('-'.repeat(70));
|
||||
hiddenFields.forEach(field => {
|
||||
console.log(` Name: ${field.name}`);
|
||||
console.log(` Value: ${field.value.substring(0, 80)}${field.value.length > 80 ? '...' : ''}`);
|
||||
console.log(` Length: ${field.value.length} chars`);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
// Show visible fields
|
||||
if (visibleFields.length > 0) {
|
||||
console.log('\n📝 VISIBLE FIELDS:');
|
||||
console.log('-'.repeat(70));
|
||||
visibleFields.forEach(field => {
|
||||
console.log(` Name: ${field.name}`);
|
||||
console.log(` Type: ${field.inputType}`);
|
||||
console.log(` ID: ${field.id}`);
|
||||
console.log(` Placeholder: ${field.placeholder}`);
|
||||
console.log(` Required: ${field.required}`);
|
||||
console.log(` Autocomplete: ${field.autocomplete}`);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
// Summary of field names
|
||||
console.log('='.repeat(70));
|
||||
console.log('FIELD NAME SUMMARY (for authentication payload):');
|
||||
console.log('='.repeat(70));
|
||||
formFields.fields.forEach(field => {
|
||||
const marker = field.inputType === 'hidden' ? '[HIDDEN]' : '[VISIBLE]';
|
||||
console.log(` ${marker} ${field.name}`);
|
||||
});
|
||||
|
||||
// Take a screenshot for visual reference
|
||||
await page.screenshot({ path: 'login-page-screenshot.png', fullPage: true });
|
||||
console.log('\n📸 Screenshot saved to: login-page-screenshot.png');
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
extractLoginForm().catch(console.error);
|
||||
@@ -3,9 +3,15 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "bun run index.ts",
|
||||
"dev": "bun run --watch index.ts"
|
||||
"dev": "bun run --watch index.ts",
|
||||
"playwright:install": "bunx playwright install",
|
||||
"cookie:get": "bun run test/get-cookies.ts",
|
||||
"test": "bun test",
|
||||
"test:auth": "bun test test/auth.spec.ts",
|
||||
"test:ui": "bunx playwright test --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@types/bun": "latest",
|
||||
"typescript-language-server": "^5.1.3"
|
||||
},
|
||||
|
||||
90
playwright-report/index.html
Normal file
90
playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
24
playwright.config.ts
Normal file
24
playwright.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineConfig, type PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './test',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: 'https://engage.nkcswx.cn',
|
||||
trace: 'on-first-retry',
|
||||
headless: true,
|
||||
},
|
||||
timeout: 60000,
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
webServer: {
|
||||
command: 'echo "No web server needed"',
|
||||
port: 0,
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
});
|
||||
32
services/cookies.json
Normal file
32
services/cookies.json
Normal file
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"name": "ASP.NET_SessionId",
|
||||
"value": "fjurweoenv1wdcvhcyvhreqh",
|
||||
"domain": "engage.nkcswx.cn",
|
||||
"path": "/",
|
||||
"expires": -1,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "usernameCookie",
|
||||
"value": "1hDdyhfXMJP9S2CpwOgJZDKOXrEEXLB%23EXOogV55NRskzh6XKDU2rym1YrGfnoklj",
|
||||
"domain": "engage.nkcswx.cn",
|
||||
"path": "/",
|
||||
"expires": 1778095681.649044,
|
||||
"httpOnly": true,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": ".ASPXFORMSAUTH",
|
||||
"value": "8E4B9D03FE08C5A2C6EB323B35110D6290CCF3408B68940F9783D1F9D37FA326A38457C956652C8A55D68218AA681485AB8C2533E4E7C85B2BF3413847009C18918281566DF940EA26761F8C0424B93AE79F519DDD0585DF19907E1F9231F5C020039960F5BFC53B7D429B1F3F83B5655F83D796",
|
||||
"domain": "engage.nkcswx.cn",
|
||||
"path": "/",
|
||||
"expires": -1,
|
||||
"httpOnly": true,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
}
|
||||
]
|
||||
188
services/playwright-auth.ts
Normal file
188
services/playwright-auth.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { chromium, type BrowserContext, type Cookie } from 'playwright';
|
||||
import { logger } from '../utils/logger';
|
||||
import * as fs from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const LOGIN_URL = 'https://engage.nkcswx.cn/Login.aspx';
|
||||
const COOKIE_FILE_PATH = resolve(import.meta.dir, 'cookies.json');
|
||||
|
||||
let _inMemoryCookies: Cookie[] | null = null;
|
||||
|
||||
/**
|
||||
* Login using Playwright and extract cookies
|
||||
*/
|
||||
export async function loginWithPlaywright(username: string, password: string): Promise<Cookie[]> {
|
||||
logger.info('Starting Playwright login process...');
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
try {
|
||||
const context = await browser.newContext({
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
logger.info(`Navigating to login page: ${LOGIN_URL}`);
|
||||
await page.goto(LOGIN_URL, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
logger.info('Login page loaded. Filling form...');
|
||||
|
||||
const usernameField = page.locator('input[name="ctl00$PageContent$loginControl$txtUN"]');
|
||||
await usernameField.fill(decodeURIComponent(username));
|
||||
|
||||
const passwordField = page.locator('input[name="ctl00$PageContent$loginControl$txtPwd"]');
|
||||
await passwordField.fill(decodeURIComponent(password));
|
||||
|
||||
const rememberMe = page.locator('input[name="ctl00$PageContent$loginControl$cbRememberMe"]');
|
||||
await rememberMe.check().catch(() => {
|
||||
logger.debug('Could not check remember me checkbox (optional)');
|
||||
});
|
||||
|
||||
const loginButton = page.locator('input[name="ctl00$PageContent$loginControl$btnLogin"]');
|
||||
logger.info('Clicking login button...');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForLoadState('networkidle', { timeout: 30000 });
|
||||
|
||||
const isLoggedIn = await checkLoginSuccess(page);
|
||||
|
||||
if (!isLoggedIn) {
|
||||
const errorMessage = await page.locator('.error, .errorMessage, [class*="error"]').first().textContent();
|
||||
throw new Error(`Login failed. Possible error: ${errorMessage || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
logger.info('Login successful! Extracting cookies...');
|
||||
|
||||
const cookies = await context.cookies();
|
||||
logger.info(`Extracted ${cookies.length} cookies`);
|
||||
|
||||
await saveCookiesToCache(cookies);
|
||||
logImportantCookies(cookies);
|
||||
|
||||
await browser.close();
|
||||
return cookies;
|
||||
} catch (error) {
|
||||
logger.error('Error during Playwright login:', error);
|
||||
await browser.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if login was successful
|
||||
*/
|
||||
async function checkLoginSuccess(page: any): Promise<boolean> {
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const currentUrl = page.url();
|
||||
const notOnLoginPage = !currentUrl.includes('Login.aspx');
|
||||
|
||||
const hasLogoutLink = await page.locator('text=Logout, text=退出,text=Sign Out').count() > 0;
|
||||
const hasWelcomeText = await page.locator('text=Welcome, text=欢迎').count() > 0;
|
||||
|
||||
return notOnLoginPage || hasLogoutLink || hasWelcomeText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log important cookies for debugging
|
||||
*/
|
||||
function logImportantCookies(cookies: Cookie[]): void {
|
||||
const importantCookieNames = [
|
||||
'ASP.NET_SessionId',
|
||||
'.ASPXFORMSAUTH',
|
||||
];
|
||||
|
||||
logger.debug('Important cookies:');
|
||||
cookies.forEach(cookie => {
|
||||
if (importantCookieNames.some(name => cookie.name.includes(name))) {
|
||||
logger.debug(` ${cookie.name}: ${cookie.value.substring(0, 50)}${cookie.value.length > 50 ? '...' : ''}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cookies from cache file
|
||||
*/
|
||||
export async function loadCachedCookies(): Promise<Cookie[] | null> {
|
||||
if (_inMemoryCookies) {
|
||||
logger.debug('Using in-memory cached cookies.');
|
||||
return _inMemoryCookies;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(COOKIE_FILE_PATH)) {
|
||||
logger.debug('Cookie cache file not found. No cached cookies loaded.');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE_PATH, 'utf-8')) as Cookie[];
|
||||
_inMemoryCookies = cookies;
|
||||
logger.debug(`Loaded ${cookies.length} cookies from file cache.`);
|
||||
return cookies;
|
||||
} catch (error: any) {
|
||||
logger.warn('Error loading cookies from file:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save cookies to cache file
|
||||
*/
|
||||
export async function saveCookiesToCache(cookies: Cookie[]): Promise<void> {
|
||||
if (!cookies || cookies.length === 0) {
|
||||
logger.warn('Attempted to save empty or null cookies. Aborting save.');
|
||||
return;
|
||||
}
|
||||
|
||||
_inMemoryCookies = cookies;
|
||||
|
||||
try {
|
||||
await fs.promises.writeFile(COOKIE_FILE_PATH, JSON.stringify(cookies, null, 2), 'utf-8');
|
||||
logger.debug('Cookies saved to file cache.');
|
||||
} catch (error: any) {
|
||||
logger.error('Error saving cookies to file:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cookie cache
|
||||
*/
|
||||
export async function clearCookieCache(): Promise<void> {
|
||||
_inMemoryCookies = null;
|
||||
|
||||
try {
|
||||
await fs.promises.unlink(COOKIE_FILE_PATH);
|
||||
logger.debug('Cookie cache file deleted.');
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
logger.error('Error deleting cookie file:', error.message);
|
||||
} else {
|
||||
logger.debug('Cookie cache file did not exist, no need to delete.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert cookies array to cookie string for axios
|
||||
*/
|
||||
export function cookiesToString(cookies: Cookie[]): string {
|
||||
return cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cookie string from cache
|
||||
*/
|
||||
export async function getCachedCookieString(): Promise<string | null> {
|
||||
const cookies = await loadCachedCookies();
|
||||
if (!cookies || cookies.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return cookiesToString(cookies);
|
||||
}
|
||||
25
startup.sh
Executable file
25
startup.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting DSAS CCA Backend..."
|
||||
|
||||
# Check if cookies exist and are valid
|
||||
if [ -f /usr/src/app/services/cookies.json ]; then
|
||||
echo "📁 Cookies file found. Checking validity..."
|
||||
|
||||
# Try to fetch a simple activity to test cookies
|
||||
# If it fails, we'll get fresh cookies
|
||||
if ! timeout 10 bun run test/test-cookies-validity.ts 2>/dev/null; then
|
||||
echo "⚠️ Cookies are invalid or expired. Getting fresh cookies..."
|
||||
bun run test/get-cookies.ts
|
||||
else
|
||||
echo "✅ Cookies are valid. Using cached cookies."
|
||||
fi
|
||||
else
|
||||
echo "📁 No cookies file found. Getting fresh cookies..."
|
||||
bun run test/get-cookies.ts
|
||||
fi
|
||||
|
||||
# Start the application
|
||||
echo "🎯 Starting application..."
|
||||
exec bun run index.ts
|
||||
118
test/auth.spec.ts
Normal file
118
test/auth.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { test, expect } from 'bun:test';
|
||||
import { chromium, type Cookie } from 'playwright';
|
||||
import * as fs from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const COOKIE_FILE_PATH = resolve(__dirname, '../services/cookies.json');
|
||||
|
||||
const testUsername = process.env.API_USERNAME || 'test@test.com';
|
||||
const testPassword = process.env.API_PASSWORD || 'test123';
|
||||
|
||||
test('should login and extract cookies successfully', async () => {
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
try {
|
||||
const context = await browser.newContext({
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('https://engage.nkcswx.cn/Login.aspx', {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
const usernameField = page.locator('input[name="ctl00$PageContent$loginControl$txtUN"]');
|
||||
await usernameField.fill(decodeURIComponent(testUsername));
|
||||
|
||||
const passwordField = page.locator('input[name="ctl00$PageContent$loginControl$txtPwd"]');
|
||||
await passwordField.fill(decodeURIComponent(testPassword));
|
||||
|
||||
const loginButton = page.locator('input[name="ctl00$PageContent$loginControl$btnLogin"]');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||||
|
||||
const cookies = await context.cookies();
|
||||
|
||||
expect(cookies).toBeDefined();
|
||||
expect(cookies.length).toBeGreaterThan(0);
|
||||
|
||||
const hasSessionCookie = cookies.some(c => c.name === 'ASP.NET_SessionId');
|
||||
expect(hasSessionCookie).toBe(true);
|
||||
|
||||
fs.writeFileSync(COOKIE_FILE_PATH, JSON.stringify(cookies, null, 2));
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}, 120000);
|
||||
|
||||
test('should load cookies from file if exists', () => {
|
||||
if (!fs.existsSync(COOKIE_FILE_PATH)) {
|
||||
throw new Error('Cookie file does not exist');
|
||||
}
|
||||
|
||||
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE_PATH, 'utf-8')) as Cookie[];
|
||||
expect(cookies.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should test cookie validity', async () => {
|
||||
if (!fs.existsSync(COOKIE_FILE_PATH)) {
|
||||
throw new Error('Cookie file does not exist');
|
||||
}
|
||||
|
||||
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE_PATH, 'utf-8')) as Cookie[];
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
try {
|
||||
const context = await browser.newContext();
|
||||
await context.addCookies(cookies);
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('https://engage.nkcswx.cn/', {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const url = page.url();
|
||||
const isRedirectedToLogin = url.includes('/Login.aspx');
|
||||
|
||||
expect(isRedirectedToLogin).toBe(false);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
test('should convert cookies to string format', () => {
|
||||
if (!fs.existsSync(COOKIE_FILE_PATH)) {
|
||||
throw new Error('Cookie file does not exist');
|
||||
}
|
||||
|
||||
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE_PATH, 'utf-8')) as Cookie[];
|
||||
|
||||
const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||
expect(cookieString).toBeDefined();
|
||||
expect(cookieString.length).toBeGreaterThan(0);
|
||||
expect(cookieString).toContain('ASP.NET_SessionId=');
|
||||
});
|
||||
|
||||
test('should clear cookie cache', () => {
|
||||
if (fs.existsSync(COOKIE_FILE_PATH)) {
|
||||
fs.unlinkSync(COOKIE_FILE_PATH);
|
||||
}
|
||||
|
||||
const exists = fs.existsSync(COOKIE_FILE_PATH);
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
24
test/get-cookies.ts
Normal file
24
test/get-cookies.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { loginWithPlaywright, saveCookiesToCache } from '../services/playwright-auth';
|
||||
|
||||
const username = process.env.API_USERNAME;
|
||||
const password = process.env.API_PASSWORD;
|
||||
|
||||
if (!username || !password) {
|
||||
console.error('❌ API_USERNAME and API_PASSWORD environment variables are required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('🔑 Starting cookie extraction...\n');
|
||||
|
||||
loginWithPlaywright(username, password)
|
||||
.then(cookies => {
|
||||
console.log(`\n✅ Extracted ${cookies.length} cookies`);
|
||||
console.log('📁 Cookies saved to: ./services/cookies.json');
|
||||
|
||||
saveCookiesToCache(cookies);
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('\n❌ Cookie extraction failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
35
test/test-cookies-validity.ts
Normal file
35
test/test-cookies-validity.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const COOKIE_FILE = './services/cookies.json';
|
||||
|
||||
async function testCookies() {
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
if (!fs.existsSync(COOKIE_FILE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE, 'utf-8'));
|
||||
const cookieString = cookies.map((c: any) => `${c.name}=${c.value}`).join('; ');
|
||||
|
||||
const url = 'https://engage.nkcswx.cn/Services/ActivitiesService.asmx/GetActivityDetails';
|
||||
const headers = {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'Cookie': cookieString,
|
||||
'User-Agent': 'Mozilla/5.0 (Bun DSAS-CCA)',
|
||||
};
|
||||
const payload = { "activityID": "3350" };
|
||||
|
||||
await axios.post(url, payload, {
|
||||
headers,
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const isValid = await testCookies();
|
||||
process.exit(isValid ? 0 : 1);
|
||||
Reference in New Issue
Block a user