diff --git a/Source/NotificationManager.ts b/Source/NotificationManager.ts index 9891829..213e81f 100644 --- a/Source/NotificationManager.ts +++ b/Source/NotificationManager.ts @@ -125,7 +125,7 @@ export class NotificationManager { const url = new URL(request.url); // Internal push channel from Process.ts. - if (url.pathname === "/notify") { + if (url.pathname === "/notify" || url.pathname === "/v1/notify") { if (this.pushToken === "" || request.headers.get("X-Notification-Token") !== this.pushToken) { return new Response("Unauthorized", {status: 401}); } diff --git a/Source/Process.ts b/Source/Process.ts index 38fc031..0816a25 100644 --- a/Source/Process.ts +++ b/Source/Process.ts @@ -1611,10 +1611,19 @@ export class Process { this.RawDatabase = Environment.DB.withSession(); } + private NormalizePathName(pathname: string): string { + if (pathname === "/" || pathname === "/v1" || pathname === "/v1/") { + return "/GetNotice"; + } + if (pathname.startsWith("/v1/")) { + return pathname.substring(3); + } + return pathname; + } + public async Process(): Promise { try { - let PathName = new URL(this.RequestData.url).pathname; - PathName = PathName === "/" ? "/GetNotice" : PathName; + let PathName = this.NormalizePathName(new URL(this.RequestData.url).pathname); PathName = PathName.substring(1); if (PathName === "GetNotice") { const notice = await this.kv.get("noticeboard"); @@ -1694,7 +1703,7 @@ export class Process { Output.Error(ResponseData); ResponseData = new Result(false, "服务器运行错误:" + String(ResponseData).split("\n")[0]); } - let pathname = new URL(this.RequestData.url).pathname; + const pathname = this.NormalizePathName(new URL(this.RequestData.url).pathname); return new Response(pathname == "/GetStd" ? this.processCppString(JSON.stringify(ResponseData)) : JSON.stringify(ResponseData), { headers: { "content-type": "application/json;charset=UTF-8" diff --git a/Source/index.ts b/Source/index.ts index 57c8e05..084f09b 100644 --- a/Source/index.ts +++ b/Source/index.ts @@ -124,7 +124,7 @@ export default { } const url = new URL(RequestData.url); - if (url.pathname === "/ws/notifications") { + if (url.pathname === "/ws/notifications" || url.pathname === "/v1/ws/notifications") { const sessionID = url.searchParams.get("SessionID") || ""; if (sessionID === "") { return new Response("Missing SessionID", {status: 401}); diff --git a/test/notification_manager.test.js b/test/notification_manager.test.js index ed8fef6..6df3e7f 100644 --- a/test/notification_manager.test.js +++ b/test/notification_manager.test.js @@ -86,3 +86,19 @@ test('rejects notify without internal token', async () => { assert.strictEqual(response.status, 401); assert.deepStrictEqual(socket.getSent(), []); }); + +test('versioned notify endpoint works the same as legacy notify endpoint', async () => { + const manager = createManager(); + const socket = createFakeWebSocket('alice'); + manager.addSession('alice', socket); + + const payload = { type: 'bbs_mention', data: { PostID: 11 } }; + const response = await manager.fetch(new Request('https://dummy/v1/notify', { + method: 'POST', + headers: { 'X-Notification-Token': 'test-push-token' }, + body: JSON.stringify({ userId: 'alice', notification: payload }), + })); + + assert.strictEqual(response.status, 200); + assert.deepStrictEqual(socket.getSent(), [JSON.stringify(payload)]); +}); diff --git a/test/process.test.js b/test/process.test.js index 1ada256..f7391ac 100644 --- a/test/process.test.js +++ b/test/process.test.js @@ -87,6 +87,238 @@ function createProcess(mocks = {}) { return proc; } +test('Process serves notice on legacy /GetNotice endpoint', async () => { + const proc = createProcess({ + req: new Request('https://example.com/GetNotice', { + method: 'GET', + headers: { "CF-Connecting-IP": "127.0.0.1" } + }), + kv: { + get: async () => 'test notice' + } + }); + + const response = await proc.Process(); + const payload = await response.json(); + assert.strictEqual(payload.Success, true); + assert.strictEqual(payload.Data.Notice, 'test notice'); +}); + +test('Process serves notice on versioned /v1/GetNotice endpoint', async () => { + const proc = createProcess({ + req: new Request('https://example.com/v1/GetNotice', { + method: 'GET', + headers: { "CF-Connecting-IP": "127.0.0.1" } + }), + kv: { + get: async () => 'test notice' + } + }); + + const response = await proc.Process(); + const payload = await response.json(); + assert.strictEqual(payload.Success, true); + assert.strictEqual(payload.Data.Notice, 'test notice'); +}); + +test('Process maps /v1 to GetNotice for versioned default endpoint', async () => { + const proc = createProcess({ + req: new Request('https://example.com/v1', { + method: 'GET', + headers: { "CF-Connecting-IP": "127.0.0.1" } + }), + kv: { + get: async () => 'test notice' + } + }); + + const response = await proc.Process(); + const payload = await response.json(); + assert.strictEqual(payload.Success, true); + assert.strictEqual(payload.Data.Notice, 'test notice'); +}); + +test('Process uses GetStd C++ string processing on /v1/GetStd', async () => { + const req = new Request('https://example.com/v1/GetStd', { + method: 'POST', + headers: { + "CF-Connecting-IP": "127.0.0.1", + "content-type": "application/json" + }, + body: JSON.stringify({ + Authentication: { SessionID: 'testsession', Username: 'testuser' }, + Data: {}, + Version: 'test', + DebugMode: false + }) + }); + const proc = createProcess({ req }); + + proc.CheckToken = test.mock.fn(async () => new Result(true, '', { Success: true })); + proc.ProcessFunctions.GetStd = test.mock.fn(async () => new Result(true, 'ok', { Code: 'line1\nline2' })); + + const response = await proc.Process(); + const responseText = await response.text(); + const expectedText = proc.processCppString(JSON.stringify(new Result(true, 'ok', { Code: 'line1\nline2' }))); + + assert.strictEqual(responseText, expectedText); +}); + +test('Process routes a generic versioned endpoint to its legacy handler', async () => { + const req = new Request('https://example.com/v1/GetUserSettings', { + method: 'POST', + headers: { + "CF-Connecting-IP": "127.0.0.1", + "content-type": "application/json" + }, + body: JSON.stringify({ + Authentication: { SessionID: 'testsession', Username: 'testuser' }, + Data: {}, + Version: 'test', + DebugMode: false + }) + }); + const proc = createProcess({ req }); + + proc.CheckToken = test.mock.fn(async () => new Result(true, '', { Success: true })); + proc.ProcessFunctions.GetUserSettings = test.mock.fn(async () => new Result(true, 'ok', { Settings: { Discussion: 'true' } })); + + const response = await proc.Process(); + const payload = await response.json(); + + assert.strictEqual(payload.Success, true); + assert.strictEqual(payload.Message, 'ok'); + assert.deepStrictEqual(payload.Data.Settings, { Discussion: 'true' }); +}); + +test('Process maps root / to GetNotice', async () => { + const proc = createProcess({ + req: new Request('https://example.com/', { + method: 'GET', + headers: { "CF-Connecting-IP": "127.0.0.1" } + }), + kv: { + get: async () => 'root notice' + } + }); + + const response = await proc.Process(); + const payload = await response.json(); + assert.strictEqual(payload.Success, true); + assert.strictEqual(payload.Data.Notice, 'root notice'); +}); + +test('Process maps /v1/ trailing slash to GetNotice', async () => { + const proc = createProcess({ + req: new Request('https://example.com/v1/', { + method: 'GET', + headers: { "CF-Connecting-IP": "127.0.0.1" } + }), + kv: { + get: async () => 'v1 slash notice' + } + }); + + const response = await proc.Process(); + const payload = await response.json(); + assert.strictEqual(payload.Success, true); + assert.strictEqual(payload.Data.Notice, 'v1 slash notice'); +}); + +test('Process serves addon script on legacy /GetAddOnScript endpoint', async () => { + const proc = createProcess({ + req: new Request('https://example.com/GetAddOnScript', { + method: 'GET', + headers: { "CF-Connecting-IP": "127.0.0.1" } + }), + kv: { + get: async () => 'console.log("addon");' + } + }); + + const response = await proc.Process(); + const payload = await response.json(); + assert.strictEqual(payload.Success, true); + assert.strictEqual(payload.Data.Script, 'console.log("addon");'); +}); + +test('Process serves addon script on versioned /v1/GetAddOnScript endpoint', async () => { + const proc = createProcess({ + req: new Request('https://example.com/v1/GetAddOnScript', { + method: 'GET', + headers: { "CF-Connecting-IP": "127.0.0.1" } + }), + kv: { + get: async () => 'console.log("addon v1");' + } + }); + + const response = await proc.Process(); + const payload = await response.json(); + assert.strictEqual(payload.Success, true); + assert.strictEqual(payload.Data.Script, 'console.log("addon v1");'); +}); + +test('Process serves image on legacy GET /GetImage endpoint', async () => { + const imageData = new Uint8Array([137, 80, 78, 71]).buffer; + const proc = createProcess({ + req: new Request('https://example.com/GetImage?ImageID=test.png', { + method: 'GET', + headers: { "CF-Connecting-IP": "127.0.0.1" } + }) + }); + + proc.ProcessFunctions.GetImage = test.mock.fn(async () => new Blob([imageData], { type: 'image/png' })); + + const response = await proc.Process(); + assert.strictEqual(response.headers.get('content-type'), 'image/png'); + assert.strictEqual(proc.ProcessFunctions.GetImage.mock.calls.length, 1); + assert.deepStrictEqual(proc.ProcessFunctions.GetImage.mock.calls[0].arguments[0], { ImageID: 'test.png' }); +}); + +test('Process serves image on versioned GET /v1/GetImage endpoint', async () => { + const imageData = new Uint8Array([137, 80, 78, 71]).buffer; + const proc = createProcess({ + req: new Request('https://example.com/v1/GetImage?ImageID=v1-test.png', { + method: 'GET', + headers: { "CF-Connecting-IP": "127.0.0.1" } + }) + }); + + proc.ProcessFunctions.GetImage = test.mock.fn(async () => new Blob([imageData], { type: 'image/png' })); + + const response = await proc.Process(); + assert.strictEqual(response.headers.get('content-type'), 'image/png'); + assert.strictEqual(proc.ProcessFunctions.GetImage.mock.calls.length, 1); + assert.deepStrictEqual(proc.ProcessFunctions.GetImage.mock.calls[0].arguments[0], { ImageID: 'v1-test.png' }); +}); + +test('Process uses GetStd C++ string processing on legacy /GetStd', async () => { + const req = new Request('https://example.com/GetStd', { + method: 'POST', + headers: { + "CF-Connecting-IP": "127.0.0.1", + "content-type": "application/json" + }, + body: JSON.stringify({ + Authentication: { SessionID: 'testsession', Username: 'testuser' }, + Data: {}, + Version: 'test', + DebugMode: false + }) + }); + const proc = createProcess({ req }); + + proc.CheckToken = test.mock.fn(async () => new Result(true, '', { Success: true })); + proc.ProcessFunctions.GetStd = test.mock.fn(async () => new Result(true, 'ok', { Code: 'int main() {\n\treturn 0;\n}' })); + + const response = await proc.Process(); + const responseText = await response.text(); + const expectedText = proc.processCppString(JSON.stringify(new Result(true, 'ok', { Code: 'int main() {\n\treturn 0;\n}' }))); + + assert.strictEqual(responseText, expectedText); +}); + test('CheckParams passes with valid data', () => { const proc = createProcess(); const result = proc.CheckParams({ a: 1, b: 'x' }, { a: 'number', b: 'string' });