Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/L2-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ jobs:
- name: Start l2-container service
run: |
docker run -d --name native-platform --link mockxconf -v ${{ github.workspace }}:/mnt/L2_CONTAINER_SHARED_VOLUME ghcr.io/rdkcentral/docker-device-mgt-service-test/native-platform:latest

- name: Copy thunder-mock-server.js file to Native Platform Container
run: |
docker cp ${{ github.workspace }}/test/test-artifacts/native-platform/thunder-mock-server.js native-platform:/usr/local/bin/thunder-mock-server.js

- name: Run thunder-mock-server.js in background
run: |
docker exec -d native-platform sh -c "node /usr/local/bin/thunder-mock-server.js > /tmp/thunder-mock.log 2>&1"

- name: Build tr69hostif and Run L2 inside Native Platform Container
run: |
Expand Down
1 change: 1 addition & 0 deletions run_l2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/bootup
pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/handlers_communications.json test/functional-tests/tests/test_handlers_communications.py
pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/deviceip.json test/functional-tests/tests/tr69hostif_deviceip.py
pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/webpa.json test/functional-tests/tests/tr69hostif_webpa.py
pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/thunder_plugin.json test/functional-tests/tests/tr69hostif_thunder_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
####################################################################################
# If not stated otherwise in this file or this component's Licenses.txt file the
# following copyright and licenses apply:
#
# Copyright 2024 RDK Management
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
####################################################################################


Feature: tr69hostif retrieves TR-181 parameters via Thunder plugin JSON-RPC

Scenario: thunder plugin account id get handler
Given When the tr69hostif binary is invoked
Then the tr69hostif should be running as a daemon
And when the tr69hostif is initialized successfully
Then the tr69hostif validation is done for Thunder plugin AccountID get handlers

Scenario: thunder plugin experience get handler
Given When the tr69hostif binary is invoked
Then the tr69hostif should be running as a daemon
And when the tr69hostif is initialized successfully
Then the tr69hostif validation is done for Thunder plugin Experience get handlers
45 changes: 45 additions & 0 deletions test/functional-tests/tests/tr69hostif_thunder_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
####################################################################################
# If not stated otherwise in this file or this component's Licenses file the
# following copyright and licenses apply:
#
# Copyright 2024 RDK Management
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
####################################################################################


import pytest

from helper_functions import *

@pytest.mark.run(order=46)
def test_ThunderPlugin_EXPERIENCE_Get_Handler():
#clear_tr69hostiflogs()

DATA_ELEMENT_NAME = "Device.DeviceInfo.X_RDKCENTRAL-COM_Experience"
EXP_MSG = "TESTOS"

rstdout = rbus_get_data(DATA_ELEMENT_NAME)
assert RBUS_EXCEPTION_STRING not in rstdout
assert EXP_MSG in rstdout

@pytest.mark.run(order=47)
def test_ThunderPlugin_AccountID_Get_Handler():
#clear_tr69hostiflogs()
DATA_ELEMENT_NAME = "Device.DeviceInfo.X_RDKCENTRAL-COM_RFC.Feature.AccountInfo.AccountID"
ACCOUNT_ID_MSG = "123456789"

rstdout = rbus_get_data(DATA_ELEMENT_NAME)
assert RBUS_EXCEPTION_STRING not in rstdout
assert ACCOUNT_ID_MSG in rstdout

292 changes: 292 additions & 0 deletions test/test-artifacts/native-platform/thunder-mock-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
#!/usr/bin/env node

/*
* SPDX-License-Identifier: Apache-2.0
*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2024 RDK Management
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Thunder JSON-RPC Mock Server
*
* Handles org.rdk.* JSON-RPC 2.0 method calls used by tr69hostif.
*
* Usage:
* node thunder-mock-server.js [--verbose]
*
* Example curl commands:
*
* Thunder JSON-RPC (application/json):
* curl -X POST http://127.0.0.1:9998/jsonrpc \
* -H 'Content-Type: application/json' \
* -d '{"jsonrpc":"2.0","id":1,"method":"org.rdk.UserSettings.getPrivacyMode"}'
*
* Thunder JSON-RPC (text/plain - also accepted):
* curl -H 'Content-Type: text/plain' \
* --data-binary '{"jsonrpc":2.0,"id":15,"method":"org.rdk.UserSettings.getPrivacyMode"}' \
* http://127.0.0.1:9998/jsonrpc
*/

'use strict';

/* coverity[missing_tls] Suppress missing_tls: This is a test mock server for localhost development only */
const http = require('http');
const url = require('url');

// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------

const VERBOSE = process.argv.includes('--verbose') || process.env.VERBOSE === '1';
const THUNDER_PORT = Number(process.env.THUNDER_PORT) || 9998;
const THUNDER_HOST = process.env.THUNDER_HOST || '127.0.0.1';

// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------

function log(tag, msg) {
if (VERBOSE) {
console.log(`[${tag}] ${msg}`);
}
}

// ---------------------------------------------------------------------------
// ─── Thunder JSON-RPC Server ────────────────────────────────────────────────
// ---------------------------------------------------------------------------

/**
* Mock response database.
* Add entries here to support additional Thunder methods.
*/
const mockResponses = {
'org.rdk.UserSettings.getPrivacyMode': {
result: 'SHARE',
description: 'User privacy mode setting',
},
'org.rdk.System.getPrivacyMode': {
result: 'SHARE',
description: 'System privacy mode setting',
},
'org.rdk.System.getPowerState': {
result: { powerState: 'STANDBY' },
description: 'Current system power state',
},
'org.rdk.NetworkManager.GetPrimaryInterface': {
result: { interface: 'eth0' },
description: 'Primary network interface',
},
'org.rdk.NetworkManager.GetIPSettings': {
result: { ipaddress: '192.168.1.100' },
description: 'IP settings for interface',
},
'org.rdk.AuthService.getServiceAccountId': {
result: { serviceAccountId: '123456789' },
description: 'Service account ID',
},
'org.rdk.AuthService.getExperience': {
result: { experience: 'TESTOS' },
description: 'Device experience profile',
},
'org.rdk.Account.getLastCheckoutResetTime': {
result: { resetTime: Math.floor(Date.now() / 1000) },
description: 'Last checkout reset timestamp',
},
};

/**
* Validate a parsed JSON-RPC request object.
* Accepts jsonrpc as either the string "2.0" or the number 2.0 so that
* clients which omit quotes (e.g. --data-binary with text/plain) still work.
*
* @param {*} data
* @returns {{ valid: boolean, error: string|null }}
*/
function validateJsonRpcRequest(data) {
if (!data || typeof data !== 'object' || Array.isArray(data)) {
return { valid: false, error: 'Request is not a valid JSON object' };
}

// Accept "2.0" (string) or 2.0 (number)
const ver = data.jsonrpc;
if (ver !== '2.0' && ver !== 2.0 && ver !== 2) {
return { valid: false, error: 'Invalid JSON-RPC version (expected "2.0")' };
}

if (!data.method || typeof data.method !== 'string') {
return { valid: false, error: 'Missing or invalid "method" field' };
}

if (data.id === undefined) {
return { valid: false, error: 'Missing required "id" field' };
}

return { valid: true, error: null };
}

function createErrorResponse(id, code, message) {
return { jsonrpc: '2.0', id: id !== undefined ? id : null, error: { code, message } };
}

function createSuccessResponse(id, result) {
return { jsonrpc: '2.0', id, result };
}

/**
* Dispatch a validated JSON-RPC request to the mock response database.
*/
function dispatchJsonRpcRequest(request) {
const { id, method, params } = request;

log('RPC', `method="${method}" id=${id} params=${JSON.stringify(params || {})}`);

const entry = mockResponses[method];
if (!entry) {
log('RPC', `Method not found: "${method}"`);
return createErrorResponse(id, -32601, `Method not found: ${method}`);
}

const response = createSuccessResponse(id, entry.result);
log('RPC', `result=${JSON.stringify(entry.result)}`);
return response;
}

/**
* Handle POST /jsonrpc - read body, parse JSON, dispatch.
*/
function handleJsonRpcPost(req, res) {
let bodyData = '';

req.on('data', (chunk) => {
if (bodyData.length + chunk.length > 1024 * 1024) {
req.pause();
res.writeHead(413, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(createErrorResponse(null, -32700, 'Request entity too large')));
return;
}
bodyData += chunk.toString('utf8');
});

req.on('end', () => {
let requestObject;

try {
requestObject = JSON.parse(bodyData);
} catch (parseError) {
log('RPC', `JSON parse failed: ${parseError.message}`);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(createErrorResponse(null, -32700, 'Parse error')));
return;
}

const validation = validateJsonRpcRequest(requestObject);
if (!validation.valid) {
log('RPC', `Validation failed: ${validation.error}`);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(createErrorResponse(requestObject.id, -32600, validation.error)));
return;
}

const response = dispatchJsonRpcRequest(requestObject);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
});

req.on('error', (error) => {
console.error(`[RPC] Stream error: ${error.message}`);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal server error' }));
});
}

/**
* Main Thunder HTTP request router.
*/
function thunderRequestHandler(req, res) {
const parsedUrl = url.parse(req.url, true);

log('THUNDER', `${req.method} ${req.url} from ${req.socket.remoteAddress}`);

if (parsedUrl.pathname === '/jsonrpc') {
if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method Not Allowed - use POST' }));
return;
}
return handleJsonRpcPost(req, res);
}

if (req.method === 'GET') {
if (parsedUrl.pathname === '/status') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'running', port: THUNDER_PORT, methods: Object.keys(mockResponses) }));
return;
}
if (parsedUrl.pathname === '/methods') {
const methods = Object.entries(mockResponses).map(([name, data]) => ({
method: name,
description: data.description,
result: data.result,
}));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(methods));
return;
}
}

res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Endpoint not found' }));
}

/**
* Start the Thunder HTTP server.
*/
function startThunderServer() {
/* coverity[missing_tls] : FP - loopback-only test mock; mirrors Thunder daemon's localhost HTTP convention */
// noinspection JSUnresolvedReference - Thunder mock for localhost development
// eslint-disable-next-line no-undef
const server = http.createServer(thunderRequestHandler);
/* coverity[missing_tls] */

server.on('error', (error) => {
console.error(`[THUNDER] Server error: ${error.message}`);
process.exit(1);
});

server.listen(THUNDER_PORT, THUNDER_HOST, () => {
console.log(`[THUNDER] JSON-RPC Mock Server running at http://${THUNDER_HOST}:${THUNDER_PORT}/jsonrpc`);
});

return server;
}

// ---------------------------------------------------------------------------
// ─── Main ───────────────────────────────────────────────────────────────────
// ---------------------------------------------------------------------------

const thunderServer = startThunderServer();

function gracefulShutdown(signal) {
console.log(`\n${signal} received, shutting down...`);
thunderServer.close(() => {
console.log('Server closed.');
process.exit(0);
});
}

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

Loading