Skip to content

danecodes/roku-mock

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@danecodes/roku-mock

Mock API server for Roku app testing. Define controlled HTTP responses, point your Roku app at the mock server, and test edge cases that are impossible to reproduce against production — empty feeds, error responses, slow backends, expired subscriptions, geo-blocked content.

Why?

You can't intercept a Roku device's HTTP traffic from outside. The app needs to know where to send requests. Most Roku dev builds support configurable API endpoints via registry values, launch params, or a dev-mode flag. This package handles the mock server side — getting the app to use it is your responsibility (via roku-odc registry injection, launch params, or app configuration).

Install

npm install @danecodes/roku-mock

Quick start

import { MockServer } from '@danecodes/roku-mock';

const mock = new MockServer({ port: 9090 });

// Static response
mock.get('/api/v1/feed', {
  status: 200,
  body: { items: [{ id: 'movie_1', title: 'Test Movie' }] },
});

// Dynamic handler with path params
mock.get('/api/v1/content/:id', (req) => {
  return { status: 200, body: { id: req.params.id, title: 'Test Movie' } };
});

mock.post('/api/v1/analytics', { status: 204 });

await mock.start();
console.log(`Mock server at ${mock.baseUrl}`);

// ... run your tests ...

await mock.stop();

The server binds to 0.0.0.0 by default so it's reachable from the Roku device on your local network. baseUrl auto-detects your machine's LAN IP.

Scenarios

Named response sets you can switch at runtime without restarting the server.

mock.scenario('happy-path', (m) => {
  m.get('/api/v1/feed', { status: 200, body: { items: [...fullFeed] } });
  m.get('/api/v1/user/profile', { status: 200, body: { name: 'Test', subscription: 'premium' } });
});

mock.scenario('empty-feed', (m) => {
  m.get('/api/v1/feed', { status: 200, body: { items: [] } });
});

mock.scenario('expired-subscription', (m) => {
  m.get('/api/v1/user/profile', { status: 200, body: { subscription: 'expired' } });
  m.get('/api/v1/content/:id', { status: 403, body: { error: 'Subscription required' } });
});

mock.scenario('geo-blocked', (m) => {
  m.get('/api/v1/content/:id', { status: 451, body: { error: 'Not available in your region' } });
});

mock.scenario('slow-backend', (m) => {
  m.get('/api/v1/feed', { status: 200, body: { items: [...fullFeed] }, latency: 5000 });
});

// Activate one
mock.activate('happy-path');
await mock.start();

// Switch at runtime — no restart needed
mock.activate('empty-feed');

// Layer scenarios — later ones override earlier for conflicting routes
mock.activate('happy-path');
mock.activate('expired-subscription');
// feed comes from happy-path, content/:id returns 403

Request recording

Track what your Roku app actually calls.

// All recorded requests
const requests = mock.requests;
// [{ method: 'GET', path: '/api/v1/feed', headers: {...}, timestamp: Date }, ...]

// Wait for a specific request (test synchronization)
const req = await mock.waitForRequest('GET', '/api/v1/feed', { timeout: 5000 });

// Assert
expect(mock.requests.filter(r => r.path === '/api/v1/analytics')).toHaveLength(1);

// Clear
mock.clearRequests();

Latency

// Global — applies to all routes
const mock = new MockServer({ port: 9090, latency: 200 });

// Per-route — overrides global
mock.get('/api/v1/feed', { status: 200, body: { items: [] }, latency: 5000 });

Config file

Define scenarios in JSON instead of code.

{
  "port": 9090,
  "host": "0.0.0.0",
  "latency": 0,
  "scenarios": {
    "happy-path": {
      "routes": [
        { "method": "GET", "path": "/api/v1/feed", "status": 200, "bodyFile": "./fixtures/feed.json" },
        { "method": "GET", "path": "/api/v1/user/profile", "status": 200, "body": { "name": "Test" } }
      ]
    },
    "empty-feed": {
      "routes": [
        { "method": "GET", "path": "/api/v1/feed", "status": 200, "body": { "items": [] } }
      ]
    }
  }
}

Route options: body (inline JSON), bodyFile (path relative to config file), latency (ms), headers (custom response headers). Path params (:id) work in config routes too.

Record/proxy mode

Point at a real backend, record all responses as JSON fixtures, then replay them offline.

import { Recorder } from '@danecodes/roku-mock';

const recorder = new Recorder({
  target: 'https://api.example.com',
  output: './fixtures/',
  port: 9090,
});

await recorder.start();
// All requests are proxied to the real backend
// Responses saved as numbered JSON files in ./fixtures/
await recorder.stop();

CLI

# Start with config file
roku-mock serve ./mock-config.json
roku-mock serve ./mock-config.json --scenario happy-path

# Inline route
roku-mock serve --get /api/feed --body '{"items":[]}'

# Options (override config file values)
roku-mock serve ./mock-config.json --port 8080 --host 0.0.0.0 --latency 200

# Record mode
roku-mock record --target https://api.example.com --output ./fixtures/

Device integration (optional)

Requires @danecodes/roku-ecp and/or @danecodes/roku-odc as optional peer dependencies.

import { MockServer, configureDevice, launchWithMock } from '@danecodes/roku-mock';
import { OdcClient } from '@danecodes/roku-odc';
import { EcpClient } from '@danecodes/roku-ecp';

const mock = new MockServer({ port: 9090 });
mock.activate('happy-path');
await mock.start();

// Set the app's API base URL via registry injection
const odc = new OdcClient('192.168.0.30');
await configureDevice(mock, odc, {
  registrySection: 'config',
  registryKey: 'apiBaseUrl',
});

// Or launch the app with the mock URL as a param
const ecp = new EcpClient('192.168.0.30');
await launchWithMock(mock, ecp, 'dev', { paramName: 'apiUrl' });
// Launches: /launch/dev?apiUrl=http://192.168.0.5:9090

License

MIT

About

Mock API server for Roku app testing

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors