Private
Public Access
Add runnable smoke-test clients for each SDK
CI / js (push) Successful in 18s
CI / go (push) Successful in 36s
CI / rust (push) Successful in 2m3s
CI / js (push) Successful in 18s
CI / go (push) Successful in 36s
CI / rust (push) Successful in 2m3s
Each package gets a small example program that exercises the real client: with no ORIGIN_SERVICE_KEY set it runs against a local mock check-in server, otherwise it checks in against ORIGIN_URL/Origin's public endpoint. Wired into CI (mock mode) as a regression check, and documented per README. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ jobs:
|
|||||||
- run: go vet ./...
|
- run: go vet ./...
|
||||||
- run: test -z "$(gofmt -l .)"
|
- run: test -z "$(gofmt -l .)"
|
||||||
- run: go test ./...
|
- run: go test ./...
|
||||||
|
- run: go run ./examples/testclient
|
||||||
|
|
||||||
js:
|
js:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -37,6 +38,7 @@ jobs:
|
|||||||
- run: npm run typecheck
|
- run: npm run typecheck
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
- run: node examples/testclient.mjs
|
||||||
|
|
||||||
rust:
|
rust:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -52,3 +54,4 @@ jobs:
|
|||||||
- run: cargo clippy --all-targets -- -D warnings
|
- run: cargo clippy --all-targets -- -D warnings
|
||||||
- run: cargo build
|
- run: cargo build
|
||||||
- run: cargo test
|
- run: cargo test
|
||||||
|
- run: cargo run --example testclient
|
||||||
|
|||||||
@@ -43,3 +43,14 @@ check-in and observe the error/response.
|
|||||||
`Hostname()`, `OutboundIP()`, `ContainerID()`, `Orchestrator()`, and
|
`Hostname()`, `OutboundIP()`, `ContainerID()`, `Orchestrator()`, and
|
||||||
`ResolveMachineID()` are exported standalone for callers that want the
|
`ResolveMachineID()` are exported standalone for callers that want the
|
||||||
detection logic without the HTTP client.
|
detection logic without the HTTP client.
|
||||||
|
|
||||||
|
## Manual smoke test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./examples/testclient # single check-in, local mock server
|
||||||
|
go run ./examples/testclient -watch # loops via Run() until Ctrl-C
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `ORIGIN_SERVICE_KEY` (and optionally `ORIGIN_URL`, `ORIGIN_TYPE`,
|
||||||
|
`ORIGIN_VERSION`, `ORIGIN_NAME`, `ORIGIN_SITE`, `ORIGIN_ENVIRONMENT`,
|
||||||
|
`ORIGIN_INTERVAL_HOURS`) to check in against a real endpoint instead.
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Command testclient is a runnable smoke test for the Go originclient SDK.
|
||||||
|
//
|
||||||
|
// Without ORIGIN_SERVICE_KEY set, it spins up a local mock check-in server
|
||||||
|
// and exercises the client against it — no network, no credentials needed.
|
||||||
|
// With ORIGIN_SERVICE_KEY set, it performs a real check-in against
|
||||||
|
// ORIGIN_URL (or Origin's public endpoint) so the SDK can be smoke-tested
|
||||||
|
// end-to-end against a live server.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
originclient "git.warky.dev/wdevs/origin_client/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
watch := flag.Bool("watch", false, "run continuously via Run() instead of a single check-in")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
cfg := originclient.Config{
|
||||||
|
URL: os.Getenv("ORIGIN_URL"),
|
||||||
|
ServiceKey: os.Getenv("ORIGIN_SERVICE_KEY"),
|
||||||
|
Type: envOr("ORIGIN_TYPE", "icy2-testclient"),
|
||||||
|
Version: envOr("ORIGIN_VERSION", "0.0.0-test"),
|
||||||
|
Name: envOr("ORIGIN_NAME", "icy2-testclient-local"),
|
||||||
|
Site: os.Getenv("ORIGIN_SITE"),
|
||||||
|
Environment: os.Getenv("ORIGIN_ENVIRONMENT"),
|
||||||
|
Logger: originclient.LoggerFunc(func(msg string, err error) { log.Printf("[warn] %s: %v", msg, err) }),
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ServiceKey == "" {
|
||||||
|
log.Println("ORIGIN_SERVICE_KEY not set — using a local mock check-in server")
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(mockHandler))
|
||||||
|
defer server.Close()
|
||||||
|
cfg.URL = server.URL
|
||||||
|
cfg.ServiceKey = "test-key"
|
||||||
|
cfg.InstallID = "test-install-id"
|
||||||
|
}
|
||||||
|
|
||||||
|
if h := os.Getenv("ORIGIN_INTERVAL_HOURS"); h != "" {
|
||||||
|
if n, err := strconv.Atoi(h); err == nil {
|
||||||
|
cfg.IntervalHours = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := originclient.New(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("originclient.New: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *watch {
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
log.Println("watching — Ctrl-C to stop")
|
||||||
|
client.Run(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.CheckinOnce(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("checkin failed: %v", err)
|
||||||
|
}
|
||||||
|
out, _ := json.MarshalIndent(resp, "", " ")
|
||||||
|
fmt.Println(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var payload originclient.Payload
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&payload)
|
||||||
|
log.Printf("mock server received check-in: type=%s name=%s install_id=%s key=%s",
|
||||||
|
payload.Type, payload.Name, payload.UniqueInstallID, r.Header.Get("X-Origin-Service-Key"))
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(originclient.Response{
|
||||||
|
Success: true,
|
||||||
|
ServiceInstanceID: 1,
|
||||||
|
UniqueInstallID: payload.UniqueInstallID,
|
||||||
|
Status: "provisioning",
|
||||||
|
PingedAt: "2026-08-01T00:00:00Z",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOr(key, def string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
@@ -45,3 +45,14 @@ check-in and observe the result/error.
|
|||||||
`hostname()`, `outboundIp()`, `containerId()`, `orchestrator()`, and
|
`hostname()`, `outboundIp()`, `containerId()`, `orchestrator()`, and
|
||||||
`resolveMachineId()` are exported standalone for callers that want the
|
`resolveMachineId()` are exported standalone for callers that want the
|
||||||
detection logic without the HTTP client.
|
detection logic without the HTTP client.
|
||||||
|
|
||||||
|
## Manual smoke test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run example # single check-in, local mock server
|
||||||
|
node examples/testclient.mjs --watch # loops via start() until Ctrl-C
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `ORIGIN_SERVICE_KEY` (and optionally `ORIGIN_URL`, `ORIGIN_TYPE`,
|
||||||
|
`ORIGIN_VERSION`, `ORIGIN_NAME`, `ORIGIN_SITE`, `ORIGIN_ENVIRONMENT`,
|
||||||
|
`ORIGIN_INTERVAL_HOURS`) to check in against a real endpoint instead.
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// Runnable smoke test for the JS/TS originclient SDK.
|
||||||
|
//
|
||||||
|
// Without ORIGIN_SERVICE_KEY set, it spins up a local mock check-in server
|
||||||
|
// and exercises the client against it — no network, no credentials needed.
|
||||||
|
// With ORIGIN_SERVICE_KEY set, it performs a real check-in against
|
||||||
|
// ORIGIN_URL (or Origin's public endpoint) so the SDK can be smoke-tested
|
||||||
|
// end-to-end against a live server.
|
||||||
|
//
|
||||||
|
// Run `npm run build` first (this imports the compiled dist/ output).
|
||||||
|
// Usage: node examples/testclient.mjs [--watch]
|
||||||
|
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import { OriginClient } from "../dist/index.js";
|
||||||
|
|
||||||
|
function envOr(key, def) {
|
||||||
|
return process.env[key] || def;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startMockServer() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
let raw = "";
|
||||||
|
req.on("data", (chunk) => {
|
||||||
|
raw += chunk;
|
||||||
|
});
|
||||||
|
req.on("end", () => {
|
||||||
|
const payload = JSON.parse(raw || "{}");
|
||||||
|
console.log(
|
||||||
|
`mock server received check-in: type=${payload.type} name=${payload.name} ` +
|
||||||
|
`install_id=${payload.unique_install_id} key=${req.headers["x-origin-service-key"]}`,
|
||||||
|
);
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
service_instance_id: 1,
|
||||||
|
unique_install_id: payload.unique_install_id,
|
||||||
|
status: "provisioning",
|
||||||
|
pinged_at: "2026-08-01T00:00:00Z",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
server.listen(0, "127.0.0.1", () => {
|
||||||
|
const address = server.address();
|
||||||
|
resolve({ server, url: `http://127.0.0.1:${address.port}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const watch = process.argv.includes("--watch");
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
url: process.env.ORIGIN_URL,
|
||||||
|
serviceKey: process.env.ORIGIN_SERVICE_KEY,
|
||||||
|
type: envOr("ORIGIN_TYPE", "icy2-testclient"),
|
||||||
|
version: envOr("ORIGIN_VERSION", "0.0.0-test"),
|
||||||
|
name: envOr("ORIGIN_NAME", "icy2-testclient-local"),
|
||||||
|
site: process.env.ORIGIN_SITE,
|
||||||
|
environment: process.env.ORIGIN_ENVIRONMENT,
|
||||||
|
logger: { warn: (msg, err) => console.warn(`[warn] ${msg}:`, err) },
|
||||||
|
};
|
||||||
|
|
||||||
|
let mockServer;
|
||||||
|
if (!config.serviceKey) {
|
||||||
|
console.log("ORIGIN_SERVICE_KEY not set — using a local mock check-in server");
|
||||||
|
const { server, url } = await startMockServer();
|
||||||
|
mockServer = server;
|
||||||
|
config.url = url;
|
||||||
|
config.serviceKey = "test-key";
|
||||||
|
config.installId = "test-install-id";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.ORIGIN_INTERVAL_HOURS) {
|
||||||
|
config.intervalHours = Number(process.env.ORIGIN_INTERVAL_HOURS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new OriginClient(config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (watch) {
|
||||||
|
console.log("watching — Ctrl-C to stop");
|
||||||
|
client.start();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
process.on("SIGINT", resolve);
|
||||||
|
process.on("SIGTERM", resolve);
|
||||||
|
});
|
||||||
|
client.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await client.checkinOnce();
|
||||||
|
console.log(JSON.stringify(resp, null, 2));
|
||||||
|
} finally {
|
||||||
|
mockServer?.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("checkin failed:", err);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
+2
-1
@@ -21,7 +21,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"example": "npm run build && node examples/testclient.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
|
|||||||
+1
-1
@@ -22,4 +22,4 @@ winreg = "0.52"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockito = "1"
|
mockito = "1"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "signal"] }
|
||||||
|
|||||||
@@ -54,6 +54,17 @@ default. `Config` implements `Default`, so use struct-update syntax
|
|||||||
`resolve_machine_id()` are exported standalone for callers that want the
|
`resolve_machine_id()` are exported standalone for callers that want the
|
||||||
detection logic without the HTTP client.
|
detection logic without the HTTP client.
|
||||||
|
|
||||||
|
## Manual smoke test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --example testclient # single check-in, local mock server
|
||||||
|
cargo run --example testclient -- --watch # loops via run() until Ctrl-C
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `ORIGIN_SERVICE_KEY` (and optionally `ORIGIN_URL`, `ORIGIN_TYPE`,
|
||||||
|
`ORIGIN_VERSION`, `ORIGIN_NAME`, `ORIGIN_SITE`, `ORIGIN_ENVIRONMENT`,
|
||||||
|
`ORIGIN_INTERVAL_HOURS`) to check in against a real endpoint instead.
|
||||||
|
|
||||||
## Platform notes
|
## Platform notes
|
||||||
|
|
||||||
macOS/Windows machine-ID detection is compiled but not exercised by CI
|
macOS/Windows machine-ID detection is compiled but not exercised by CI
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
//! Runnable smoke test for the Rust origin-client SDK.
|
||||||
|
//!
|
||||||
|
//! Without ORIGIN_SERVICE_KEY set, it spins up a local mock check-in server
|
||||||
|
//! and exercises the client against it — no network, no credentials needed.
|
||||||
|
//! With ORIGIN_SERVICE_KEY set, it performs a real check-in against
|
||||||
|
//! ORIGIN_URL (or Origin's public endpoint) so the SDK can be smoke-tested
|
||||||
|
//! end-to-end against a live server.
|
||||||
|
//!
|
||||||
|
//! Usage: `cargo run --example testclient [-- --watch]`
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use origin_client::{Client, Config, FnLogger};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
fn env_or(key: &str, default: &str) -> String {
|
||||||
|
env::var(key).unwrap_or_else(|_| default.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let watch = env::args().any(|a| a == "--watch");
|
||||||
|
|
||||||
|
let mut cfg = Config {
|
||||||
|
url: env::var("ORIGIN_URL").ok(),
|
||||||
|
service_key: env::var("ORIGIN_SERVICE_KEY").unwrap_or_default(),
|
||||||
|
type_: env_or("ORIGIN_TYPE", "icy2-testclient"),
|
||||||
|
version: env_or("ORIGIN_VERSION", "0.0.0-test"),
|
||||||
|
name: env_or("ORIGIN_NAME", "icy2-testclient-local"),
|
||||||
|
site: env::var("ORIGIN_SITE").ok(),
|
||||||
|
environment: env::var("ORIGIN_ENVIRONMENT").ok(),
|
||||||
|
logger: Arc::new(FnLogger(|msg: &str, err| {
|
||||||
|
eprintln!("[warn] {msg}: {err:?}");
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Held so the mock server stays alive for the client's lifetime; unused
|
||||||
|
// in real-endpoint mode.
|
||||||
|
let mut _mock_server = None;
|
||||||
|
|
||||||
|
if cfg.service_key.is_empty() {
|
||||||
|
println!("ORIGIN_SERVICE_KEY not set — using a local mock check-in server");
|
||||||
|
let mut server = mockito::Server::new_async().await;
|
||||||
|
server
|
||||||
|
.mock("POST", "/")
|
||||||
|
.with_status(200)
|
||||||
|
.with_header("content-type", "application/json")
|
||||||
|
.with_body(
|
||||||
|
r#"{"success":true,"service_instance_id":1,"unique_install_id":"test-install-id","status":"provisioning","pinged_at":"2026-08-01T00:00:00Z"}"#,
|
||||||
|
)
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cfg.url = Some(server.url());
|
||||||
|
cfg.service_key = "test-key".to_string();
|
||||||
|
cfg.install_id = Some("test-install-id".to_string());
|
||||||
|
_mock_server = Some(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(h) = env::var("ORIGIN_INTERVAL_HOURS") {
|
||||||
|
if let Ok(n) = h.parse() {
|
||||||
|
cfg.interval_hours = Some(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = Client::new(cfg).expect("origin_client::Config validation failed");
|
||||||
|
|
||||||
|
if watch {
|
||||||
|
println!("watching — Ctrl-C to stop");
|
||||||
|
let cancel = CancellationToken::new();
|
||||||
|
let cancel_clone = cancel.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::signal::ctrl_c().await.ok();
|
||||||
|
cancel_clone.cancel();
|
||||||
|
});
|
||||||
|
client.run(cancel).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match client.checkin_once().await {
|
||||||
|
Ok(resp) => {
|
||||||
|
println!("{resp:#?}");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("checkin failed: {err}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user