Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Source/NotificationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}
Expand Down
15 changes: 12 additions & 3 deletions Source/Process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response> {
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");
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion Source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down
16 changes: 16 additions & 0 deletions test/notification_manager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Comment on lines +90 to +99

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a negative test for the versioned /v1/notify path to mirror the legacy unauthorized-case coverage.

To fully mirror the legacy behavior and protect against regressions in the aliasing, please also add a test where /v1/notify is called with a missing or invalid X-Notification-Token, asserting it returns 401 and does not send anything on the socket, matching the existing /notify unauthorized test.

Suggested implementation:

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)]);
});

test('versioned notify endpoint rejects requests without a valid notification token', 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',
    // No X-Notification-Token header, mirroring the legacy unauthorized test
    body: JSON.stringify({ userId: 'alice', notification: payload }),
  }));

  assert.strictEqual(response.status, 401);
  assert.deepStrictEqual(socket.getSent(), []);
  1. Ensure that the inserted positive test body (the body: JSON.stringify(...), assert.strictEqual(...), and assert.deepStrictEqual(...) lines) exactly matches what is already in your file; adjust indentation or trailing commas as needed.
  2. If the legacy /notify unauthorized test uses a different payload shape or additional assertions, you may want to mirror those details in the new /v1/notify unauthorized test for stricter parity.

}));

assert.strictEqual(response.status, 200);
assert.deepStrictEqual(socket.getSent(), [JSON.stringify(payload)]);
});
232 changes: 232 additions & 0 deletions test/process.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Comment on lines +101 to +102

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add assertions on HTTP status (and possibly headers) to more fully validate the behavior of the new aliased endpoints.

For these new tests (e.g. /GetNotice, /v1/GetNotice, /, /v1/, /GetAddOnScript, /v1/GetAddOnScript), we only check the JSON body. Please also assert response.status (e.g. assert.strictEqual(response.status, 200)) and any key headers like content-type so we verify the aliased endpoints remain fully equivalent, not just in payload.

Suggested implementation:

    const response = await proc.Process();
    assert.strictEqual(response.status, 200);
    assert.match(response.headers.get('content-type') ?? '', /^application\/json\b/i);

    const payload = await response.json();
    assert.strictEqual(payload.Success, true);
    assert.strictEqual(payload.Data.Notice, 'test notice');
  1. For the other aliased endpoint tests mentioned (/v1/GetNotice, /, /v1/, /GetAddOnScript, /v1/GetAddOnScript), add the same pair of assertions immediately after const response = await proc.Process();:
    • assert.strictEqual(response.status, 200);
    • assert.match(response.headers.get('content-type') ?? '', /^application\/json\b/i); (or an equivalent check appropriate to the endpoint, e.g., for script/text responses you may want text/javascript or text/html instead of application/json).
  2. Ensure that these header/status assertions are added before reading the body with response.json() or response.text() so failures are surfaced early and consistently across tests.

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: {},
Comment on lines +141 to +150

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding a negative/edge-case test for versioned GetStd to ensure aliasing doesn’t bypass error handling.

You’re already verifying that /GetStd and /v1/GetStd share the same happy-path C++ processing. Please also add a failing case (e.g., CheckToken failure or ProcessFunctions.GetStd error) for /v1/GetStd to confirm the alias follows the same error path and still returns C++-processed errors rather than plain JSON.

Suggested implementation:

    const payload = await response.json();
    assert.strictEqual(payload.Success, true);
    assert.strictEqual(payload.Data.Notice, 'test notice');
});

test('Process uses GetStd C++ error handling on /v1/GetStd for failing C++ processing', 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',
        }),
    });

    const proc = new Process(req, {
        CheckToken: async () => ({
            Success: true,
            Data: { Username: 'testuser' },
        }),
        ProcessFunctions: {
            GetStd: {
                // Simulate a C++-side error path for GetStd
                post: async () => {
                    return {
                        ok: false,
                        status: 500,
                        json: async () => ({
                            Success: false,
                            Error: 'forced C++ GetStd failure',
                        }),
                    };
                },
            },
        },
    });

    const response = await proc.Process();
    const payload = await response.json();

    // Ensure the alias /v1/GetStd follows the same C++ error path as /GetStd
    assert.strictEqual(payload.Success, false);
    assert.strictEqual(payload.Error, 'forced C++ GetStd failure');
});

test('Process uses GetStd C++ string processing on /v1/GetStd', async () => {

You may need to adjust the new test to match your existing testing conventions and error shapes:

  1. Shape of CheckToken result:

    • If other tests return a differently shaped object (e.g. { Success: false, ErrorCode: '...' }), align this mock with that structure.
  2. Shape of ProcessFunctions.GetStd.post result:

    • I assumed it returns a Response-like object with ok, status, and json().
    • If your existing GetStd tests use a different mock (e.g. directly returning { Success: false, ... }), copy that pattern here and keep the failure semantics identical.
  3. Error assertions:

    • If your C++-processed errors use different fields (ErrorMessage, ErrorCode, Data.Error, etc.), update the assertions to mirror the expectations from your existing failing /GetStd test, so that /v1/GetStd is verified against the same error contract.

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' }));
Comment on lines +262 to +271

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Strengthen the GetImage tests by verifying the response body, not just headers and arguments.

Right now the test only checks that GetImage is called with { ImageID } and that content-type is image/png. To fully validate the behavior, also read the response body (e.g. via await response.arrayBuffer() or await response.blob()) and assert its length or bytes match the imageData you set up. This confirms the Blob from ProcessFunctions.GetImage is actually streamed to the client.

Suggested implementation:

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();

    // Validate the handler was invoked with the expected arguments
    assert.strictEqual(proc.ProcessFunctions.GetImage.mock.calls.length, 1);
    assert.deepStrictEqual(proc.ProcessFunctions.GetImage.mock.calls[0][0], {
        ImageID: 'test.png'
    });

    // Validate response headers
    assert.strictEqual(response.headers.get('content-type'), 'image/png');

    // Validate that the response body matches the image data we set up
    const bodyBuffer = await response.arrayBuffer();
    assert.strictEqual(bodyBuffer.byteLength, imageData.byteLength);
    assert.deepStrictEqual(
        new Uint8Array(bodyBuffer),
        new Uint8Array(imageData)
    );
});

If your test framework exposes mock call metadata differently than mock.calls, adjust the assertions on proc.ProcessFunctions.GetImage.mock.calls accordingly (e.g., mock.calls[0].arguments or similar), but keep the body validation via response.arrayBuffer() and Uint8Array comparison as implemented.


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' });
Expand Down
Loading