more integration tests

This commit is contained in:
seavor 2026-04-16 12:40:47 -05:00
parent 4b5f66d497
commit decebc25c7
192 changed files with 3090 additions and 1657 deletions

View file

@ -1,30 +1,44 @@
// Connection-lifecycle scenarios. Exercises the full transport handshake
// from `webClient.connect()` through `onopen`, ServerIdentification, and
// from webClient.connect() through onopen, ServerIdentification, and
// disconnect — with only the browser WebSocket constructor mocked.
import { create } from '@bufbuild/protobuf';
import { describe, expect, it } from 'vitest';
import { App, Data } from '@app/types';
import { Data } from '@app/types';
import { store } from '@app/store';
import { StatusEnum } from '@app/websocket';
import { PROTOCOL_VERSION } from '../../src/websocket/config';
import { getMockWebSocket, getWebClient, openMockWebSocket } from './helpers/setup';
import {
getMockWebSocket,
getWebClient,
openMockWebSocket,
setPendingOptions,
connectAndHandshake,
} from './helpers/setup';
import type { WebSocketConnectOptions } from '@app/websocket';
import { WebSocketConnectReason } from '@app/websocket';
import {
buildSessionEventMessage,
deliverMessage,
} from './helpers/protobuf-builders';
import { findLastSessionCommand } from './helpers/command-capture';
function loginOptions(overrides: Partial<{ userName: string; password: string }> = {}) {
function loginOptions(overrides: Partial<{ userName: string; password: string }> = {}): WebSocketConnectOptions {
return {
reason: App.WebSocketConnectReason.LOGIN,
reason: WebSocketConnectReason.LOGIN,
host: 'localhost',
port: '4748',
userName: overrides.userName ?? 'alice',
password: overrides.password ?? 'secret',
} as const;
};
}
function connectWithOptions(opts: WebSocketConnectOptions): void {
setPendingOptions(opts);
getWebClient().connect({ host: opts.host, port: opts.port });
}
function serverIdentification(
@ -43,47 +57,45 @@ function serverIdentification(
describe('connection lifecycle', () => {
it('flips status through CONNECTING → CONNECTED on socket open', () => {
getWebClient().connect(loginOptions());
connectWithOptions(loginOptions());
expect(store.getState().server.status.connectionAttemptMade).toBe(true);
openMockWebSocket();
expect(store.getState().server.status.state).toBe(App.StatusEnum.CONNECTED);
expect(store.getState().server.status.state).toBe(StatusEnum.CONNECTED);
expect(store.getState().server.status.description).toBe('Connected');
});
it('routes a matching ServerIdentification into LOGGING_IN and sends Command_Login', () => {
getWebClient().connect(loginOptions({ userName: 'alice' }));
connectWithOptions(loginOptions({ userName: 'alice' }));
openMockWebSocket();
deliverMessage(serverIdentification());
expect(store.getState().server.status.state).toBe(App.StatusEnum.LOGGING_IN);
expect(store.getState().server.status.state).toBe(StatusEnum.LOGGING_IN);
expect(store.getState().server.info.name).toBe('TestServer');
expect(store.getState().server.info.version).toBe('2.8.0');
const { value, cmdId } = findLastSessionCommand(Data.Command_Login_ext);
expect(value.userName).toBe('alice');
expect(cmdId).toBeGreaterThan(0);
expect(getWebClient().options).toBeNull();
});
it('disconnects on protocol version mismatch without sending a login command', () => {
getWebClient().connect(loginOptions());
connectWithOptions(loginOptions());
openMockWebSocket();
deliverMessage(serverIdentification(PROTOCOL_VERSION + 1));
const mock = getMockWebSocket();
expect(mock.close).toHaveBeenCalled();
expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED);
expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED);
expect(() => findLastSessionCommand(Data.Command_Login_ext)).toThrow();
});
it('times out when onopen never fires within the keepalive window', () => {
getWebClient().connect(loginOptions());
connectWithOptions(loginOptions());
const mock = getMockWebSocket();
expect(mock.close).not.toHaveBeenCalled();
@ -91,11 +103,11 @@ describe('connection lifecycle', () => {
vi.advanceTimersByTime(5000);
expect(mock.close).toHaveBeenCalled();
expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED);
expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED);
});
it('releases keep-alive ping loop on explicit disconnect', () => {
getWebClient().connect(loginOptions());
connectWithOptions(loginOptions());
openMockWebSocket();
deliverMessage(serverIdentification());
@ -103,6 +115,20 @@ describe('connection lifecycle', () => {
getWebClient().disconnect();
expect(mock.close).toHaveBeenCalled();
expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED);
expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED);
});
});
it('drops pending commands and clears state on unexpected socket close', () => {
connectAndHandshake();
// A login command is now pending (sent during handshake)
expect(() => findLastSessionCommand(Data.Command_Login_ext)).not.toThrow();
// Simulate unexpected socket close
const mock = getMockWebSocket();
mock.readyState = 3;
mock.onclose?.({ code: 1006, reason: '', wasClean: false } as CloseEvent);
expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED);
});
});