Add Android node foundation with FCM push support
This commit is contained in:
@@ -275,4 +275,44 @@ describe('node handlers', () => {
|
||||
expect((result as { result: { push: { tokenPreview: string } } }).result.push.tokenPreview).toContain('abcd1234');
|
||||
expect(states.get('conn-1')?.pushToken?.provider).toBe('apns');
|
||||
});
|
||||
|
||||
it('accepts fcm push token registration for android companions', async () => {
|
||||
const states = new Map<string, NodeConnectionState>([['conn-1', {
|
||||
node: {
|
||||
nodeId: 'android-node',
|
||||
role: 'companion',
|
||||
protocolVersion: 1,
|
||||
capabilities: ['notifications'],
|
||||
registeredAt: Date.now(),
|
||||
},
|
||||
}]]);
|
||||
const handlers = createNodeHandlers({
|
||||
enabled: true,
|
||||
locationEnabled: true,
|
||||
pushEnabled: true,
|
||||
allowedRoles: ['companion'],
|
||||
featureGates: {},
|
||||
getConnectionState: (connectionId) => states.get(connectionId),
|
||||
setNodeRegistration: () => {},
|
||||
setNodeLocation: () => {},
|
||||
setNodeStatus: () => {},
|
||||
setNodePushToken: (connectionId, pushToken) => {
|
||||
const prior = states.get(connectionId) ?? {};
|
||||
states.set(connectionId, { ...prior, pushToken });
|
||||
},
|
||||
});
|
||||
|
||||
const result = await handlers['node.push_token.set']({
|
||||
id: 9,
|
||||
method: 'node.push_token.set',
|
||||
params: {
|
||||
connectionId: 'conn-1',
|
||||
provider: 'fcm',
|
||||
token: 'fcm_abcdefghijklmnopqrstuvwxyz123456',
|
||||
},
|
||||
});
|
||||
expect((result as { result: { updated: boolean } }).result.updated).toBe(true);
|
||||
expect(states.get('conn-1')?.pushToken?.provider).toBe('fcm');
|
||||
expect(states.get('conn-1')?.pushToken?.environment).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,10 +50,10 @@ export interface NodeStatus {
|
||||
}
|
||||
|
||||
export interface NodePushToken {
|
||||
provider: 'apns';
|
||||
provider: 'apns' | 'fcm';
|
||||
token: string;
|
||||
topic?: string;
|
||||
environment: 'sandbox' | 'production';
|
||||
environment?: 'sandbox' | 'production';
|
||||
registeredAt: number;
|
||||
}
|
||||
|
||||
@@ -279,10 +279,10 @@ export function createNodeHandlers(deps: NodeHandlerDeps) {
|
||||
}
|
||||
|
||||
const pushToken: NodePushToken = {
|
||||
provider: 'apns',
|
||||
provider: parsed.provider,
|
||||
token: parsed.token,
|
||||
topic: parsed.topic || undefined,
|
||||
environment: parsed.environment ?? 'production',
|
||||
environment: parsed.provider === 'apns' ? (parsed.environment ?? 'production') : undefined,
|
||||
registeredAt: Date.now(),
|
||||
};
|
||||
deps.setNodePushToken(parsed.connectionId, pushToken);
|
||||
|
||||
@@ -46,7 +46,7 @@ export interface NodePushTokenSummary {
|
||||
provider: NodePushToken['provider'];
|
||||
tokenPreview: string;
|
||||
topic?: string;
|
||||
environment: NodePushToken['environment'];
|
||||
environment?: NodePushToken['environment'];
|
||||
registeredAt: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ describe('protocol', () => {
|
||||
it('rejects invalid node push token params', () => {
|
||||
expect(parseNodePushTokenSetParams({
|
||||
connectionId: 'conn-1',
|
||||
provider: 'fcm',
|
||||
provider: 'webpush',
|
||||
token: 'abcd1234abcd1234abcd1234abcd1234',
|
||||
})).toBeNull();
|
||||
expect(parseNodePushTokenSetParams({
|
||||
@@ -247,6 +247,21 @@ describe('protocol', () => {
|
||||
token: 'abcd1234abcd1234abcd1234abcd1234',
|
||||
})).toBeNull();
|
||||
});
|
||||
|
||||
it('parses valid fcm token params for android nodes', () => {
|
||||
const parsed = parseNodePushTokenSetParams({
|
||||
connectionId: 'conn-2',
|
||||
provider: 'fcm',
|
||||
token: 'fcm_abcdefghijklmnopqrstuvwxyz123456',
|
||||
});
|
||||
expect(parsed).toEqual({
|
||||
connectionId: 'conn-2',
|
||||
provider: 'fcm',
|
||||
token: 'fcm_abcdefghijklmnopqrstuvwxyz123456',
|
||||
topic: undefined,
|
||||
environment: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeResponse', () => {
|
||||
|
||||
@@ -46,7 +46,7 @@ export interface NodeStatusSetParams {
|
||||
|
||||
export interface NodePushTokenSetParams {
|
||||
connectionId: string;
|
||||
provider: 'apns';
|
||||
provider: 'apns' | 'fcm';
|
||||
token: string;
|
||||
topic?: string;
|
||||
environment?: 'sandbox' | 'production';
|
||||
@@ -309,7 +309,7 @@ export function parseNodePushTokenSetParams(params: unknown): NodePushTokenSetPa
|
||||
if (typeof p.connectionId !== 'string' || !p.connectionId.trim()) {
|
||||
return null;
|
||||
}
|
||||
if (p.provider !== 'apns') {
|
||||
if (p.provider !== 'apns' && p.provider !== 'fcm') {
|
||||
return null;
|
||||
}
|
||||
if (typeof p.token !== 'string' || p.token.trim().length < 16) {
|
||||
@@ -324,7 +324,7 @@ export function parseNodePushTokenSetParams(params: unknown): NodePushTokenSetPa
|
||||
|
||||
return {
|
||||
connectionId: p.connectionId,
|
||||
provider: 'apns',
|
||||
provider: p.provider,
|
||||
token: p.token.trim(),
|
||||
topic: typeof p.topic === 'string' ? p.topic.trim() : undefined,
|
||||
environment: p.environment as NodePushTokenSetParams['environment'] | undefined,
|
||||
|
||||
@@ -868,4 +868,56 @@ describe('GatewayServer node registration and capability negotiation', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('supports android fcm push token registration', async () => {
|
||||
if (!LISTEN_ALLOWED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = await new Promise<WebSocket>((resolve, reject) => {
|
||||
const c = new WebSocket(`ws://127.0.0.1:${NODE_PORT}`);
|
||||
c.on('open', () => resolve(c));
|
||||
c.on('error', reject);
|
||||
});
|
||||
|
||||
try {
|
||||
const registered = await sendAndReceive(ws, {
|
||||
id: 40,
|
||||
method: 'node.register',
|
||||
params: {
|
||||
nodeId: 'node-android',
|
||||
role: 'companion',
|
||||
protocolVersion: 1,
|
||||
capabilities: ['notifications'],
|
||||
},
|
||||
});
|
||||
expect(((registered as GatewayResponse).result as { registered: boolean }).registered).toBe(true);
|
||||
|
||||
const push = await sendAndReceive(ws, {
|
||||
id: 41,
|
||||
method: 'node.push_token.set',
|
||||
params: {
|
||||
provider: 'fcm',
|
||||
token: 'fcm_abcdefghijklmnopqrstuvwxyz123456',
|
||||
},
|
||||
});
|
||||
expect(((push as GatewayResponse).result as { updated: boolean }).updated).toBe(true);
|
||||
|
||||
const nodes = await sendAndReceive(ws, {
|
||||
id: 42,
|
||||
method: 'system.nodes',
|
||||
params: { role: 'companion', limit: 20 },
|
||||
});
|
||||
const list = ((nodes as GatewayResponse).result as {
|
||||
nodes: Array<{ nodeId: string; push?: { provider: string; environment?: string } }>;
|
||||
}).nodes;
|
||||
const androidNode = list.find((entry) => entry.nodeId === 'node-android');
|
||||
expect(androidNode?.push?.provider).toBe('fcm');
|
||||
expect(androidNode?.push?.environment).toBeUndefined();
|
||||
} finally {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -236,7 +236,7 @@ export class GatewayServer {
|
||||
provider: NonNullable<NodeConnectionState['pushToken']>['provider'];
|
||||
tokenPreview: string;
|
||||
topic?: string;
|
||||
environment: NonNullable<NodeConnectionState['pushToken']>['environment'];
|
||||
environment?: NonNullable<NodeConnectionState['pushToken']>['environment'];
|
||||
registeredAt: number;
|
||||
};
|
||||
}> = [];
|
||||
|
||||
Reference in New Issue
Block a user