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(); ServerDispatch.connectionFailed();
} }
testConnectionSuccessful(serverOptions: number): void { testConnectionSuccessful(supportsHashedPassword: boolean): void {
ServerDispatch.testConnectionSuccessful(serverOptions); ServerDispatch.testConnectionSuccessful(supportsHashedPassword);
} }
testConnectionFailed(): void { testConnectionFailed(): void {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -205,23 +205,21 @@ describe('WebClient', () => {
expect(MockWS).toHaveBeenCalledWith(expect.stringMatching(/:\/\/server\.example\.com\/servatrice$/)); 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); client.testConnect(target);
const data = buildServerIdentificationMessage({ const data = buildServerIdentificationMessage({
serverOptions: Event_ServerIdentification_ServerOptions.SupportsPasswordHash, serverOptions: Event_ServerIdentification_ServerOptions.SupportsPasswordHash,
}); });
wsMockInstance.onmessage({ data: data.buffer }); wsMockInstance.onmessage({ data: data.buffer });
expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith( expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(true);
Event_ServerIdentification_ServerOptions.SupportsPasswordHash,
);
expect(wsMockInstance.close).toHaveBeenCalled(); 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); client.testConnect(target);
const data = buildServerIdentificationMessage({ serverOptions: 0 }); const data = buildServerIdentificationMessage({ serverOptions: 0 });
wsMockInstance.onmessage({ data: data.buffer }); 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', () => { 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 { ProtobufService } from './services/ProtobufService';
import { WebSocketService } from './services/WebSocketService'; import { WebSocketService } from './services/WebSocketService';
import { buildWebSocketUrl } from './utils/buildWebSocketUrl'; import { buildWebSocketUrl } from './utils/buildWebSocketUrl';
import { passwordSaltSupported } from './utils/passwordHasher';
export class WebClient { export class WebClient {
private static _instance: WebClient | null = null; private static _instance: WebClient | null = null;
@ -96,10 +97,12 @@ export class WebClient {
this.testSocket = socket; this.testSocket = socket;
// "Green" means reachable AND speaking a compatible Cockatrice protocol. // "Green" means reachable AND speaking a compatible Cockatrice protocol.
// Waiting for Event_ServerIdentification lets us carry serverOptions back // Waiting for Event_ServerIdentification lets us read the hashed-password
// to the UI so naked-password hosts can be distinguished without a login. // 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; let resolved = false;
const resolve = (ok: boolean, serverOptions = 0): void => { const resolve = (ok: boolean, supportsHashedPassword = false): void => {
if (resolved) { if (resolved) {
return; return;
} }
@ -109,7 +112,7 @@ export class WebClient {
// already taken over and we'd race a stale result into its pending-ref. // already taken over and we'd race a stale result into its pending-ref.
if (this.testSocket === socket) { if (this.testSocket === socket) {
if (ok) { if (ok) {
this.response.session.testConnectionSuccessful(serverOptions); this.response.session.testConnectionSuccessful(supportsHashedPassword);
} else { } else {
this.response.session.testConnectionFailed(); this.response.session.testConnectionFailed();
} }
@ -135,7 +138,7 @@ export class WebClient {
resolve(false); resolve(false);
return; return;
} }
resolve(true, ident.serverOptions); resolve(true, passwordSaltSupported(ident.serverOptions));
} catch { } catch {
resolve(false); resolve(false);
} }

View file

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