Files
simplex-chat/packages/simplex-chat-nodejs/tests/api.unit.test.ts
T
sh c8f20bcc91 core, libs: configurable queue size, library fixes (#7542)
* core: add chat_migrate_init_queue FFI export

* bots: fix BadgeServiceErrorCode API type

* nodejs: pass required command fields

* nodejs: fix migration error types

* nodejs: install libsimplex from SIMPLEX_LIBS_DIR

* nodejs: add queue size option

* nodejs: regenerate docs

* python: add queue size option

* bots: pass incognito in APIConnect

* nodejs: accept documented success responses

* python: accept documented success responses

* nodejs: dispatch each bot message once

* nodejs: fix startChat events loop lifecycle

* nodejs, python: parse multi-line bot commands

* nodejs: fix file buffer handling in addon

* nodejs: keep events loop when chat stop fails

* python: fix send_and_wait race, load lib off loop

* python: make queue size export optional

* nodejs: receive events on a dedicated thread

* nodejs: release haskell thread after receive

* python: receive on a dedicated thread per chat

* python: test receive shutdown order

* nodejs: one receive thread per chat controller

* nodejs: harden receiver shutdown and tests

* nodejs: stop chat before closing store

* python: stop chat before closing store

* nodejs, python: harden close regression and retry

* python: free results with ucrt on windows

* nodejs: enable c++ exceptions on mac and windows
2026-09-19 12:18:46 +01:00

103 lines
4.5 KiB
TypeScript

import {ChatResponse, T} from "@simplex-chat/types"
import * as api from "../src/api"
import * as core from "../src/core"
const user = {userId: 1} as T.User
async function chatWithResponse(response: object): Promise<api.ChatApi> {
jest.spyOn(core, "chatMigrateInit").mockResolvedValue(BigInt(1))
jest.spyOn(core, "chatSendCmd").mockResolvedValue(response as ChatResponse)
return api.ChatApi.init({type: "sqlite", filePrefix: "unused"})
}
afterEach(() => jest.restoreAllMocks())
describe("documented success responses", () => {
it("apiChatItemReaction returns the reaction", async () => {
const reaction = {chatReaction: {reaction: {type: "emoji", emoji: "👍"}}}
const chat = await chatWithResponse({type: "chatItemReaction", user, added: true, reaction})
await expect(chat.apiChatItemReaction(T.ChatType.Direct, 1, 2, true, {type: "emoji", emoji: "👍"})).resolves.toEqual(reaction)
})
it("apiUpdateChatItem accepts chatItemNotChanged", async () => {
const chatItem = {meta: {itemId: 2}}
const chat = await chatWithResponse({type: "chatItemNotChanged", user, chatItem: {chatItem}})
await expect(chat.apiUpdateChatItem(T.ChatType.Direct, 1, 2, {type: "text", text: "same"}, false)).resolves.toEqual(chatItem)
})
it("apiSetProfileAddress accepts userProfileNoChange", async () => {
const chat = await chatWithResponse({type: "userProfileNoChange", user})
await expect(chat.apiSetProfileAddress(1, true)).resolves.toEqual({updateSuccesses: 0, updateFailures: 0, changedContacts: []})
})
it("apiReceiveFile reports a file cancelled by sender", async () => {
const chat = await chatWithResponse({type: "rcvFileAcceptedSndCancelled", user, rcvFileTransfer: {}})
await expect(chat.apiReceiveFile(3)).rejects.toThrow("file cancelled by sender")
})
})
describe("startChat lifecycle", () => {
function chatWithResponses(...responses: object[]): Promise<api.ChatApi> {
jest.spyOn(core, "chatMigrateInit").mockResolvedValue(BigInt(1))
jest.spyOn(core, "chatRecvMsgWait").mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(undefined), 10)))
const send = jest.spyOn(core, "chatSendCmd")
for (const r of responses) send.mockResolvedValueOnce(r as ChatResponse)
return api.ChatApi.init({type: "sqlite", filePrefix: "unused"})
}
it("rejects a second start", async () => {
const chat = await chatWithResponses({type: "chatStarted"}, {type: "chatStopped"})
await chat.startChat()
await expect(chat.startChat()).rejects.toThrow("chat already started")
await chat.stopChat()
})
it("stops the events loop when start fails", async () => {
const chat = await chatWithResponses({type: "chatCmdError"})
await expect(chat.startChat()).rejects.toThrow("error starting chat")
expect(chat.started).toBe(false)
})
it("rejects start after close", async () => {
const chat = await chatWithResponses({type: "chatStopped"})
jest.spyOn(core, "chatCloseStore").mockResolvedValue()
await chat.close()
await expect(chat.startChat()).rejects.toThrow("chat api controller not initialized")
})
it("stops the chat before closing the store", async () => {
const chat = await chatWithResponses()
const calls: string[] = []
jest.mocked(core.chatSendCmd).mockImplementation(async (_ctrl, cmd) => {
calls.push(`send ${cmd}`)
return {type: "chatStopped"} as ChatResponse
})
jest.spyOn(core, "chatCloseStore").mockImplementation(async () => { calls.push("closeStore") })
await chat.close()
expect(calls).toEqual(["send /_stop", "closeStore"])
expect(chat.initialized).toBe(false)
})
it("does not close the store when stopping fails", async () => {
const chat = await chatWithResponses({type: "chatCmdError"})
const closeStore = jest.spyOn(core, "chatCloseStore").mockResolvedValue()
await expect(chat.close()).rejects.toThrow("error stopping chat")
expect(closeStore).not.toHaveBeenCalled()
expect(chat.initialized).toBe(true)
})
it("reports stop failures as stop errors", async () => {
const chat = await chatWithResponses({type: "chatStarted"}, {type: "chatCmdError"}, {type: "chatStopped"})
await chat.startChat()
await expect(chat.stopChat()).rejects.toThrow("error stopping chat")
expect(chat.started).toBe(true)
await chat.stopChat()
})
it("receives with a 500 ms wait", async () => {
const chat = await chatWithResponses()
await chat.recvChatEvent()
expect(core.chatRecvMsgWait).toHaveBeenCalledWith(BigInt(1), 500_000)
})
})