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
38 changes: 33 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,42 @@

jobs:

test_and_lint:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Install dependencies
run: |
go get golang.org/x/tools/cmd/goimports@latest
go get github.com/golangci/golangci-lint/cmd/golangci-lint@latest

- name: Lint
run: |
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.2.2
make lint || true

- name: Security scan
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck -show verbose ./... || true

- name: Test
run: |
(cd test; npm install)
make build test

build_on_macos:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

runs-on: macos-latest
permissions:
contents: read
needs: [test_and_lint]
steps:
- uses: actions/checkout@v4

Expand All @@ -25,17 +56,12 @@
- name: Install dependencies
run: |
go get golang.org/x/tools/cmd/goimports@latest
go get github.com/golangci/golangci-lint/cmd/golangci-lint@latest

- name: Set version
if: github.ref_type == 'tag'
run: |
sed -i '' 's/version = \"dev\"/version = \"${{ github.ref_name }}\"/' main.go

- name: Lint
run: |
make lint || true

- name: Build
run: |
GOARCH=arm64 go build -ldflags="-s -w" -o mobilecli-arm64
Expand All @@ -58,6 +84,7 @@
runs-on: ubuntu-latest
permissions:
contents: read
needs: [test_and_lint]
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -97,6 +124,7 @@
runs-on: windows-latest
permissions:
contents: read
needs: [test_and_lint]
steps:
- uses: actions/checkout@v4
- name: Set up Go
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mobilecli
screenshot.png
.DS_Store
**/node_modules
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ build:

test:
go test ./... -v -race
(cd test; npm test)

lint:
$(GOPATH)/bin/golangci-lint run
Expand Down
4 changes: 4 additions & 0 deletions test/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"slow": 1000,
"timeout": 30000
}
126 changes: 126 additions & 0 deletions test/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { spawn, ChildProcess, spawnSync } from 'child_process';
import { describe, it, before, after } from 'mocha';
import { strict as assert } from 'assert';
import path from 'path';

describe('jsonrpc api common', () => {
let serverProcess: ChildProcess;
const serverUrl = 'http://localhost:12000';
const timeout = 10000;

before(async function () {
this.timeout(timeout);

// Start the mobilecli server
serverProcess = spawn('../mobilecli', ['server', 'start'], {
cwd: path.dirname(__filename),
stdio: ['ignore', 'pipe', 'pipe']
});

// Wait for server to start by polling the endpoint
await waitForServer(serverUrl, 8000); // Wait up to 8 seconds for server to start
});

after(() => {
// Clean up the server process
if (serverProcess) {
serverProcess.kill('SIGTERM');
}
});

it('should return status "ok" from root endpoint', async () => {
const response = await fetch(serverUrl);
assert.equal(response.status, 200);

const data = await response.json();
assert.deepEqual(data, { status: 'ok' });
});

it('should return method_not_allowed for GET /rpc endpoint', async () => {
const response = await fetch(`${serverUrl}/rpc`);
assert.equal(response.status, 405);
});

it('should return method_not_allowed for POST /rpc endpoint', async () => {
const response = await fetch(`${serverUrl}/rpc`, { method: 'POST' });
assert.equal(response.status, 200);
const json = await response.json();
assert.equal(json.jsonrpc, '2.0');
assert.equal(json.error.code, -32700);
assert.equal(json.error.data, 'expecting jsonrpc payload');
});

it('should return error if jsonrpc is not 2.0', async () => {
const response = await fetch(`${serverUrl}/rpc`, { method: 'POST', body: JSON.stringify({ jsonrpc: '1.0' }) });
assert.equal(response.status, 200);
const json = await response.json();
assert.equal(json.jsonrpc, '2.0');
assert.equal(json.error.code, -32600);
assert.equal(json.error.data, "'jsonrpc' must be '2.0'");
});

it('should return error if id is missing', async () => {
const response = await fetch(`${serverUrl}/rpc`, { method: 'POST', body: JSON.stringify({ jsonrpc: '2.0', method: 'devices', params: {} }) });
assert.equal(response.status, 200);
const json = await response.json();
assert.equal(json.jsonrpc, '2.0');
assert.equal(json.error.code, -32600);
assert.equal(json.error.data, "'id' field is required");
});

false && it('should accept "devices" call without params', async () => {
const response = await fetch(`${serverUrl}/rpc`, {
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
method: 'devices',
id: 1
}),
signal: AbortSignal.timeout(30000),
});

assert.equal(response.status, 200);
const json = await response.json();
assert.equal(json.jsonrpc, '2.0');
assert.equal(json.id, 1);
assert.ok(Array.isArray(json.result.devices));
});

it('should return error for "device_info" call without params', async () => {
const response = await fetch(`${serverUrl}/rpc`, {
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
method: 'device_info',
id: 1
})
});

assert.equal(response.status, 200);
const json = await response.json();
assert.equal(json.jsonrpc, '2.0');
assert.equal(json.id, 1);
assert.equal(json.error.code, -32000);
assert.equal(json.error.data, "'params' is required with fields: deviceId");
});

const waitForServer = async (url: string, timeoutMs: number): Promise<void> => {
const startTime = Date.now();
const expirationTime = startTime + timeoutMs;

while (Date.now() < expirationTime) {
try {
const response = await fetch(url);
if (response.status === 200) {
return; // Server is ready
}
} catch (error) {
// Server not ready yet, continue waiting
}

await new Promise(resolve => setTimeout(resolve, 100));
}

throw new Error(`Server did not start within ${timeoutMs}ms`);
};
});
Loading
Loading