fix tests

This commit is contained in:
seavor 2026-04-20 22:17:32 -05:00
parent a75abe1454
commit 88489ea2eb
9 changed files with 27 additions and 28 deletions

View file

@ -30,8 +30,8 @@ export class SessionResponseImpl implements WebsocketTypes.ISessionResponse {
ServerDispatch.connectionFailed();
}
testConnectionSuccessful(serverOptions: number): void {
ServerDispatch.testConnectionSuccessful(serverOptions);
testConnectionSuccessful(supportsHashedPassword: boolean): void {
ServerDispatch.testConnectionSuccessful(supportsHashedPassword);
}
testConnectionFailed(): void {

View file

@ -6,7 +6,6 @@ import { LoadingState, useKnownHosts, useReduxEffect, useWebClient } from '@app/
import { getHostPort, HostDTO } from '@app/services';
import { ServerDispatch, ServerSelectors, ServerTypes, useAppSelector } from '@app/store';
import { App } from '@app/types';
import { passwordSaltSupported } from '@app/websocket';
export enum TestConnection {
TESTING = 'testing',
@ -83,14 +82,13 @@ export function useKnownHostsComponent({
testConnection(selectedHost);
}, [selectedHost]);
useReduxEffect<{ serverOptions: number }>(({ payload: { serverOptions } }) => {
useReduxEffect<{ supportsHashedPassword: boolean }>(({ payload: { supportsHashedPassword } }) => {
const host = pendingTestRef.current;
if (!host) {
return;
}
pendingTestRef.current = null;
const supportsHashedPassword = passwordSaltSupported(serverOptions);
if (host.id != null && host.supportsHashedPassword !== supportsHashedPassword) {
void knownHosts.update(host.id, { supportsHashedPassword });
}

View file

@ -42,9 +42,9 @@ describe('Actions', () => {
});
it('testConnectionSuccessful', () => {
expect(Actions.testConnectionSuccessful({ serverOptions: 1 })).toEqual({
expect(Actions.testConnectionSuccessful({ supportsHashedPassword: true })).toEqual({
type: Types.TEST_CONNECTION_SUCCESSFUL,
payload: { serverOptions: 1 },
payload: { supportsHashedPassword: true },
});
});

View file

@ -56,9 +56,9 @@ describe('Dispatch', () => {
});
it('testConnectionSuccessful dispatches Actions.testConnectionSuccessful()', () => {
Dispatch.testConnectionSuccessful(3);
Dispatch.testConnectionSuccessful(true);
expect(mockDispatch).toHaveBeenCalledWith(
Actions.testConnectionSuccessful({ serverOptions: 3 }),
Actions.testConnectionSuccessful({ supportsHashedPassword: true }),
);
});

View file

@ -25,8 +25,8 @@ export const Dispatch = {
testConnectionStarted: () => {
store.dispatch(Actions.testConnectionStarted());
},
testConnectionSuccessful: (serverOptions: number) => {
store.dispatch(Actions.testConnectionSuccessful({ serverOptions }));
testConnectionSuccessful: (supportsHashedPassword: boolean) => {
store.dispatch(Actions.testConnectionSuccessful({ supportsHashedPassword }));
},
testConnectionFailed: () => {
store.dispatch(Actions.testConnectionFailed());

View file

@ -129,11 +129,11 @@ export const serverSlice = createSlice({
state.testConnectionStatus = 'testing';
},
// `serverOptions` is typed on the action so `useReduxEffect` subscribers
// (see useKnownHostsComponent) can read it from the dispatched action —
// it's deliberately not stored in state since only the lifecycle matters
// here; the capability bitmask is persisted per-host to Dexie.
testConnectionSuccessful: (state, _action: PayloadAction<{ serverOptions: number }>) => {
// `supportsHashedPassword` is typed on the action so `useReduxEffect`
// subscribers (see useKnownHostsComponent) can persist it to the host
// record in Dexie. It's deliberately not stored in redux state since
// only the lifecycle matters here; per-host capability lives in Dexie.
testConnectionSuccessful: (state, _action: PayloadAction<{ supportsHashedPassword: boolean }>) => {
state.testConnectionStatus = 'success';
},

View file

@ -205,23 +205,21 @@ describe('WebClient', () => {
expect(MockWS).toHaveBeenCalledWith(expect.stringMatching(/:\/\/server\.example\.com\/servatrice$/));
});
it('dispatches testConnectionSuccessful with serverOptions on ServerIdentification', () => {
it('dispatches testConnectionSuccessful with supportsHashedPassword=true when the bit is set', () => {
client.testConnect(target);
const data = buildServerIdentificationMessage({
serverOptions: Event_ServerIdentification_ServerOptions.SupportsPasswordHash,
});
wsMockInstance.onmessage({ data: data.buffer });
expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(
Event_ServerIdentification_ServerOptions.SupportsPasswordHash,
);
expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(true);
expect(wsMockInstance.close).toHaveBeenCalled();
});
it('reports success with serverOptions=0 for naked-password servers', () => {
it('dispatches testConnectionSuccessful with supportsHashedPassword=false for naked-password servers', () => {
client.testConnect(target);
const data = buildServerIdentificationMessage({ serverOptions: 0 });
wsMockInstance.onmessage({ data: data.buffer });
expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(0);
expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(false);
});
it('fails on protocol-version mismatch instead of reporting success', () => {

View file

@ -18,6 +18,7 @@ import { StatusEnum } from './types/StatusEnum';
import { ProtobufService } from './services/ProtobufService';
import { WebSocketService } from './services/WebSocketService';
import { buildWebSocketUrl } from './utils/buildWebSocketUrl';
import { passwordSaltSupported } from './utils/passwordHasher';
export class WebClient {
private static _instance: WebClient | null = null;
@ -96,10 +97,12 @@ export class WebClient {
this.testSocket = socket;
// "Green" means reachable AND speaking a compatible Cockatrice protocol.
// Waiting for Event_ServerIdentification lets us carry serverOptions back
// to the UI so naked-password hosts can be distinguished without a login.
// Waiting for Event_ServerIdentification lets us read the hashed-password
// capability before the user ever logs in. The bitmask is resolved here
// (the websocket layer owns protocol details) so downstream consumers
// receive a domain-level boolean instead of a raw integer.
let resolved = false;
const resolve = (ok: boolean, serverOptions = 0): void => {
const resolve = (ok: boolean, supportsHashedPassword = false): void => {
if (resolved) {
return;
}
@ -109,7 +112,7 @@ export class WebClient {
// already taken over and we'd race a stale result into its pending-ref.
if (this.testSocket === socket) {
if (ok) {
this.response.session.testConnectionSuccessful(serverOptions);
this.response.session.testConnectionSuccessful(supportsHashedPassword);
} else {
this.response.session.testConnectionFailed();
}
@ -135,7 +138,7 @@ export class WebClient {
resolve(false);
return;
}
resolve(true, ident.serverOptions);
resolve(true, passwordSaltSupported(ident.serverOptions));
} catch {
resolve(false);
}

View file

@ -55,7 +55,7 @@ export interface ISessionResponse {
loginSuccessful(options: LoginSuccessContext): void;
loginFailed(): void;
connectionFailed(): void;
testConnectionSuccessful(serverOptions: number): void;
testConnectionSuccessful(supportsHashedPassword: boolean): void;
testConnectionFailed(): void;
updateBuddyList(buddyList: ServerInfo_User[]): void;
addToBuddyList(user: ServerInfo_User): void;