单测
Some checks failed
test / Run tests (push) Failing after 15s
build / Build (push) Failing after 22s
Some checks failed
test / Run tests (push) Failing after 15s
build / Build (push) Failing after 22s
This commit is contained in:
parent
fd2aba4286
commit
131f1bda40
@ -1,5 +1,6 @@
|
|||||||
import EventEmitter from "eventemitter3";
|
import EventEmitter from "eventemitter3";
|
||||||
import { Message, MessageConnect, MessageSend } from "./server";
|
import { Message, MessageConnect, MessageSend } from "./server";
|
||||||
|
import { sleep } from "@App/pkg/utils/utils";
|
||||||
|
|
||||||
export class MockMessageConnect implements MessageConnect {
|
export class MockMessageConnect implements MessageConnect {
|
||||||
constructor(protected EE: EventEmitter) {}
|
constructor(protected EE: EventEmitter) {}
|
||||||
@ -24,16 +25,16 @@ export class MockMessageConnect implements MessageConnect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MockMessageSend implements MessageSend {
|
export class MockMessageSend implements MessageSend {
|
||||||
constructor(
|
constructor(protected EE: EventEmitter) {}
|
||||||
protected EE: EventEmitter,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
connect(data: any): Promise<MessageConnect> {
|
connect(data: any): Promise<MessageConnect> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const EE = new EventEmitter();
|
const EE = new EventEmitter();
|
||||||
const con = new MockMessageConnect(EE);
|
const con = new MockMessageConnect(EE);
|
||||||
this.EE.emit("connect", data, con);
|
|
||||||
resolve(con);
|
resolve(con);
|
||||||
|
sleep(1).then(() => {
|
||||||
|
this.EE.emit("connect", data, con);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,27 +3,78 @@ import { Group, MessageConnect } from "@Packages/message/server";
|
|||||||
export default class GMApi {
|
export default class GMApi {
|
||||||
constructor(private group: Group) {}
|
constructor(private group: Group) {}
|
||||||
|
|
||||||
xmlHttpRequest(params: GMSend.XHRDetails, con: MessageConnect | null) {
|
dealXhrResponse(con: MessageConnect, details: GMSend.XHRDetails, event: string, xhr: XMLHttpRequest, data?: any) {
|
||||||
|
const finalUrl = xhr.responseURL || details.url;
|
||||||
|
// 判断是否有headerFlag-final-url,有则替换finalUrl
|
||||||
|
let response: GMTypes.XHRResponse = {
|
||||||
|
finalUrl,
|
||||||
|
readyState: <any>xhr.readyState,
|
||||||
|
status: xhr.status,
|
||||||
|
statusText: xhr.statusText,
|
||||||
|
// responseHeaders: xhr.getAllResponseHeaders().replace(removeXCat, ""),
|
||||||
|
responseType: details.responseType,
|
||||||
|
};
|
||||||
|
if (data) {
|
||||||
|
response = Object.assign(response, data);
|
||||||
|
}
|
||||||
|
con.sendMessage({
|
||||||
|
action: event,
|
||||||
|
data: response,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlHttpRequest(details: GMSend.XHRDetails, con: MessageConnect | null) {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open(params.method || "GET", params.url);
|
xhr.open(details.method || "GET", details.url);
|
||||||
// 添加header
|
// 添加header
|
||||||
if (params.headers) {
|
if (details.headers) {
|
||||||
for (const key in params.headers) {
|
for (const key in details.headers) {
|
||||||
xhr.setRequestHeader(key, params.headers[key]);
|
xhr.setRequestHeader(key, details.headers[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
xhr.onload = function () {
|
xhr.onload = () => {
|
||||||
console.log(xhr.getAllResponseHeaders());
|
this.dealXhrResponse(con!, details, "onload", xhr);
|
||||||
con?.sendMessage({
|
|
||||||
action: "onload",
|
|
||||||
data: {
|
|
||||||
status: xhr.status,
|
|
||||||
statusText: xhr.statusText,
|
|
||||||
response: xhr.responseText,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
xhr.onloadstart = () => {
|
||||||
|
this.dealXhrResponse(con!, details, "onloadstart", xhr);
|
||||||
|
};
|
||||||
|
xhr.onloadend = () => {
|
||||||
|
this.dealXhrResponse(con!, details, "onloadend", xhr);
|
||||||
|
};
|
||||||
|
xhr.onabort = () => {
|
||||||
|
this.dealXhrResponse(con!, details, "onabort", xhr);
|
||||||
|
};
|
||||||
|
xhr.onerror = () => {
|
||||||
|
this.dealXhrResponse(con!, details, "onerror", xhr);
|
||||||
|
};
|
||||||
|
xhr.onprogress = (event) => {
|
||||||
|
const respond: GMTypes.XHRProgress = {
|
||||||
|
done: xhr.DONE,
|
||||||
|
lengthComputable: event.lengthComputable,
|
||||||
|
loaded: event.loaded,
|
||||||
|
total: event.total,
|
||||||
|
totalSize: event.total,
|
||||||
|
};
|
||||||
|
this.dealXhrResponse(con!, details, "onprogress", xhr, respond);
|
||||||
|
};
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
this.dealXhrResponse(con!, details, "onreadystatechange", xhr);
|
||||||
|
};
|
||||||
|
xhr.ontimeout = () => {
|
||||||
|
con?.sendMessage({ action: "ontimeout", data: {} });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (details.timeout) {
|
||||||
|
xhr.timeout = details.timeout;
|
||||||
|
}
|
||||||
|
if (details.overrideMimeType) {
|
||||||
|
xhr.overrideMimeType(details.overrideMimeType);
|
||||||
|
}
|
||||||
xhr.send();
|
xhr.send();
|
||||||
|
con?.onDisconnect(() => {
|
||||||
|
xhr.abort();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -137,7 +137,6 @@ export default class GMApi {
|
|||||||
return Promise.reject(new Error("param is failed"));
|
return Promise.reject(new Error("param is failed"));
|
||||||
}
|
}
|
||||||
const params = request.params[0] as GMSend.XHRDetails;
|
const params = request.params[0] as GMSend.XHRDetails;
|
||||||
console.log("xml request", request, con);
|
|
||||||
// 先处理unsafe hearder
|
// 先处理unsafe hearder
|
||||||
// 关联自己生成的请求id与chrome.webRequest的请求id
|
// 关联自己生成的请求id与chrome.webRequest的请求id
|
||||||
const requestId = 10000 + (await incr(Cache.getInstance(), "gmXhrRequestId", 1));
|
const requestId = 10000 + (await incr(Cache.getInstance(), "gmXhrRequestId", 1));
|
||||||
@ -155,13 +154,15 @@ export default class GMApi {
|
|||||||
details.responseHeaders?.forEach((header) => {
|
details.responseHeaders?.forEach((header) => {
|
||||||
responseHeader += header.name + ": " + header.value + "\n";
|
responseHeader += header.name + ": " + header.value + "\n";
|
||||||
});
|
});
|
||||||
console.log("处理", details, responseHeader);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// 再发送到offscreen, 处理请求
|
// 再发送到offscreen, 处理请求
|
||||||
const offscreenCon = await connect(this.sender, "gmApi/xmlHttpRequest", request.params[0]);
|
const offscreenCon = await connect(this.sender, "gmApi/xmlHttpRequest", request.params[0]);
|
||||||
offscreenCon.onMessage((msg) => {
|
offscreenCon.onMessage((msg: { action: string; data: any }) => {
|
||||||
console.log("offscreenCon", msg);
|
// 发送到content
|
||||||
|
// 替换msg.data.responseHeaders
|
||||||
|
msg.data.responseHeaders = responseHeader;
|
||||||
|
con.sendMessage(msg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,3 +211,9 @@ export function checkSilenceUpdate(oldMeta: Metadata, newMeta: Metadata): boolea
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sleep(time: number) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, time);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -219,8 +219,37 @@ export default class GMApi {
|
|||||||
let connect: MessageConnect;
|
let connect: MessageConnect;
|
||||||
this.connect("GM_xmlhttpRequest", [param]).then((con) => {
|
this.connect("GM_xmlhttpRequest", [param]).then((con) => {
|
||||||
connect = con;
|
connect = con;
|
||||||
con.onMessage((data) => {
|
con.onMessage((data: { action: string; data: any }) => {
|
||||||
console.log("data", data);
|
// 处理返回
|
||||||
|
switch (data.action) {
|
||||||
|
case "onload":
|
||||||
|
details.onload?.(data.data);
|
||||||
|
break;
|
||||||
|
case "onloadend":
|
||||||
|
details.onloadend?.(data.data);
|
||||||
|
break;
|
||||||
|
case "onloadstart":
|
||||||
|
details.onloadstart?.(data.data);
|
||||||
|
break;
|
||||||
|
case "onprogress":
|
||||||
|
details.onprogress?.(data.data);
|
||||||
|
break;
|
||||||
|
case "onreadystatechange":
|
||||||
|
details.onreadystatechange && details.onreadystatechange(data.data);
|
||||||
|
break;
|
||||||
|
case "ontimeout":
|
||||||
|
details.ontimeout?.();
|
||||||
|
break;
|
||||||
|
case "onerror":
|
||||||
|
details.onerror?.("");
|
||||||
|
break;
|
||||||
|
case "onabort":
|
||||||
|
details.onabort?.();
|
||||||
|
break;
|
||||||
|
case "onstream":
|
||||||
|
// controller?.enqueue(new Uint8Array(resp.data));
|
||||||
|
break;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
203
tests/runtime/gm_api.test.ts
Normal file
203
tests/runtime/gm_api.test.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import { Script, ScriptDAO } from "@App/app/repo/scripts";
|
||||||
|
import GMApi from "@App/runtime/content/gm_api";
|
||||||
|
import chromeMock from "@Packages/chrome-extension-mock";
|
||||||
|
import { initTestEnv, initTestGMApi } from "@Tests/utils";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import { newMockXhr } from "mock-xmlhttprequest";
|
||||||
|
import { beforeAll, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
initTestEnv();
|
||||||
|
|
||||||
|
const msg = initTestGMApi();
|
||||||
|
|
||||||
|
const script: Script = {
|
||||||
|
uuid: randomUUID(),
|
||||||
|
name: "test",
|
||||||
|
metadata: {
|
||||||
|
grant: [
|
||||||
|
// gm xhr
|
||||||
|
"GM_xmlhttpRequest",
|
||||||
|
],
|
||||||
|
connect: ["example.com"],
|
||||||
|
},
|
||||||
|
namespace: "",
|
||||||
|
type: 1,
|
||||||
|
status: 1,
|
||||||
|
sort: 0,
|
||||||
|
runStatus: "running",
|
||||||
|
createtime: 0,
|
||||||
|
checktime: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await new ScriptDAO().save(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GM xmlHttpRequest", () => {
|
||||||
|
const gmApi = new GMApi(msg);
|
||||||
|
//@ts-ignore
|
||||||
|
gmApi.scriptRes = {
|
||||||
|
uuid: script.uuid,
|
||||||
|
};
|
||||||
|
const mockXhr = newMockXhr();
|
||||||
|
mockXhr.onSend = async (request) => {
|
||||||
|
switch (request.url) {
|
||||||
|
case "https://www.example.com/":
|
||||||
|
return request.respond(200, {}, "example");
|
||||||
|
case window.location.href:
|
||||||
|
return request.respond(200, {}, "location");
|
||||||
|
case "https://example.com/json":
|
||||||
|
return request.respond(200, { "Content-Type": "application/json" }, JSON.stringify({ test: 1 }));
|
||||||
|
case "https://www.example.com/header":
|
||||||
|
if (request.requestHeaders.getHeader("x-nonce") !== "123456") {
|
||||||
|
return request.respond(403, {}, "bad");
|
||||||
|
}
|
||||||
|
return request.respond(200, {}, "header");
|
||||||
|
case "https://www.example.com/unsafeHeader":
|
||||||
|
if (
|
||||||
|
request.requestHeaders.getHeader("Origin") !== "https://example.com" ||
|
||||||
|
request.requestHeaders.getHeader("Cookie") !== "website=example.com"
|
||||||
|
) {
|
||||||
|
return request.respond(400, {}, "bad request");
|
||||||
|
}
|
||||||
|
return request.respond(200, { "Set-Cookie": "test=1" }, "unsafeHeader");
|
||||||
|
case "https://www.wexample.com/unsafeHeader/cookie":
|
||||||
|
if (request.requestHeaders.getHeader("Cookie") !== "test=1") {
|
||||||
|
return request.respond(400, {}, "bad request");
|
||||||
|
}
|
||||||
|
return request.respond(200, {}, "unsafeHeader/cookie");
|
||||||
|
}
|
||||||
|
if (request.method === "POST") {
|
||||||
|
switch (request.url) {
|
||||||
|
case "https://example.com/form":
|
||||||
|
if (request.body.get("blob")) {
|
||||||
|
return request.respond(
|
||||||
|
200,
|
||||||
|
{ "Content-Type": "text/html" },
|
||||||
|
// mock 一个blob对象
|
||||||
|
{
|
||||||
|
text: () => Promise.resolve("form"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return request.respond(400, {}, "bad");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request.respond(200, {}, "test");
|
||||||
|
};
|
||||||
|
global.XMLHttpRequest = mockXhr;
|
||||||
|
it("get", () => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
gmApi.GM_xmlhttpRequest({
|
||||||
|
url: "https://www.example.com",
|
||||||
|
onreadystatechange: (resp) => {
|
||||||
|
if (resp.readyState === 4 && resp.status === 200) {
|
||||||
|
expect(resp.responseText).toBe("example");
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("post数据和blob", () => {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("blob", new Blob(["blob"], { type: "text/html" }));
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
gmApi.GM_xmlhttpRequest({
|
||||||
|
url: "https://example.com/form",
|
||||||
|
method: "POST",
|
||||||
|
data: form,
|
||||||
|
responseType: "blob",
|
||||||
|
onreadystatechange: async (resp) => {
|
||||||
|
if (resp.readyState === 4 && resp.status === 200) {
|
||||||
|
expect(resp.responseText).toBe("form");
|
||||||
|
expect(await (<Blob>resp.response).text()).toBe("form");
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// xml原版是没有responseText的,但是tampermonkey有,恶心的兼容性
|
||||||
|
it("json", async () => {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
gmApi.GM_xmlhttpRequest({
|
||||||
|
url: "https://example.com/json",
|
||||||
|
method: "GET",
|
||||||
|
responseType: "json",
|
||||||
|
onload: (resp) => {
|
||||||
|
// @ts-ignore
|
||||||
|
expect(resp.response.test).toBe(1);
|
||||||
|
expect(resp.responseText).toBe('{"test":1}');
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// bad json
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
gmApi.GM_xmlhttpRequest({
|
||||||
|
url: "https://www.example.com/",
|
||||||
|
method: "GET",
|
||||||
|
responseType: "json",
|
||||||
|
onload: (resp) => {
|
||||||
|
expect(resp.response).toBeUndefined();
|
||||||
|
expect(resp.responseText).toBe("example");
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("header", async () => {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
gmApi.GM_xmlhttpRequest({
|
||||||
|
url: "https://www.example.com/header",
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"x-nonce": "123456",
|
||||||
|
},
|
||||||
|
onload: (resp) => {
|
||||||
|
expect(resp.responseText).toBe("header");
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("unsafeHeader", async () => {
|
||||||
|
global.XMLHttpRequest = chromeMock.webRequest.mockXhr(mockXhr);
|
||||||
|
// 模拟header
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
gmApi.GM_xmlhttpRequest({
|
||||||
|
url: "https://www.example.com/unsafeHeader",
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Origin: "https://example.com",
|
||||||
|
},
|
||||||
|
onload: (resp) => {
|
||||||
|
expect(resp.responseText).toBe("unsafeHeader");
|
||||||
|
expect(resp.responseHeaders?.indexOf("set-cookie")).not.toBe(-1);
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("unsafeHeader/cookie", async () => {
|
||||||
|
// global.XMLHttpRequest = chromeMock.webRequest.mockXhr(mockXhr);
|
||||||
|
// 模拟header
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
gmApi.GM_xmlhttpRequest({
|
||||||
|
url: "https://www.wexample.com/unsafeHeader/cookie",
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Cookie: "test=1",
|
||||||
|
},
|
||||||
|
anonymous: true,
|
||||||
|
onload: (resp) => {
|
||||||
|
expect(resp.responseText).toBe("unsafeHeader/cookie");
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user