@crvouga/嘲笑鸟-服务-极地
本地模拟器。 真正的API合同. 部分 嘲笑鸟。 。 。 .
WIP Polar AccessLink v3 模拟器:OAuth 2.0授权代码,用户注册和练习端点,带有30天的能见度窗口.
安装
bun add @crvouga/mockingbird-service-polar
使用量
import { createServer } from "@crvouga/mockingbird-service-polar/server"
// Test setup: a local Polar. The user is registered with the client ten days ago and has an
// exercise uploaded afterwards. Seeds, presets and the clock are the only emulator-specific lines.
const now = Date.now()
const server = await createServer({
registrations: [
{ xUserId: 1001, memberId: "m-1", registeredAt: new Date(now - 10 * 86_400_000).toISOString() },
],
exercises: [
{
xUserId: 1001,
id: "2AC312F",
upload_time: new Date(now - 86_400_000).toISOString(),
start_time: "2026-01-30T08:00:00",
duration: "PT2H44M",
},
],
})
// Application code: the same three origins, each pointed at the emulator.
const apiOrigin = server.url
const tokenUrl = `${server.url}/v2/oauth2/token`
const authorizeUrl = `${server.url}/oauth2/authorization`
const consent = await fetch(
`${authorizeUrl}?response_type=code&client_id=mock_client&redirect_uri=https://app.example.com/cb&state=s1&scope=accesslink.read_all`,
{ redirect: "manual" },
)
const code = new URL(consent.headers.get("location") ?? "").searchParams.get("code") ?? ""
const tokenResponse = await fetch(tokenUrl, {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
authorization: `Basic ${btoa("mock_client:mock_client_secret")}`,
},
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: "https://app.example.com/cb",
}),
})
const { access_token } = (await tokenResponse.json()) as { access_token: string }
const exercises = await fetch(`${apiOrigin}/v3/exercises?samples=false&zones=false&route=false`, {
headers: { authorization: `Bearer ${access_token}`, accept: "application/json" },
})
console.log(await exercises.json())
await server.close()
运行 mockingbird-polar serve --port 12157。消费者有三种来源设置:设置 API 来源,符号 URL(<origin>/v2/oauth2/token)和授权的URL(<origin>/oauth2/authorization)给模仿者. 一个模拟器服务于所有三个路由集,所以它们共享了源. 同意页是一个浏览器重定向,所以浏览器无法发送 x-mockingbird-namespace: 使用 /__admin/ns/<name> 前缀( e)<origin>/__admin/ns/<name>/oauth2/authorization为它选择一个命名空间.
路线和状态
默认: 客户端 mock_client 编号 : mock_client_secret (任何方向调整 URI 除非客户列表 redirectUris),用户 x_user_id 1001(未注册)和访问符 mock_polar_token 给那个用户. GET /oauth2/authorization 重定向到 redirect_uri?code=<code>&state=<state> 作为第一个(或 consentUser)用户;代码是一次性的,在模拟器时钟上10分钟良好. POST /v2/oauth2/token 以窗体和 Basic 客户验证并回答 {access_token, token_type: "bearer", expires_in: 31535999, x_user_id} (输入;无) refresh_token,没有 scope) (中文(简体) ). Tokens 永远不会过期;只有在被撤销时才会停止工作. 代码的第二次使用, 不同的客户端代码或不匹配 redirect_uri 为 400 invalid_grant;未知客户端或错误的秘密是400 invalid_client; 非形体, 缺失 grant_type,或除 authorization_code (包括 refresh_token)为400 invalid_request。 。 。 .
POST /v3/users 与 {"member-id": "..."} 在仿真器时钟的即时端用符的客户端登记符的用户,并用配置文件回答200个(polar-user-id, (中文(简体) ), member-id, (中文(简体) ), registration-date, (中文(简体) ), first-name(中文(简体) ). 已经注册的用户,或 member-id 该客户持有的另一个用户是409个。 没有尸体 member-id 400;非JSON体为415.
GET /v3/exercises 返回在注册时或注册后上传的、在模拟时钟前不超过30天(确切的30天仍在返回),最古老的上传功能的 JSON 系列; [] 当没有. GET /v3/exercises/{id} 返回一个,或404用于未知的id,另一个用户的练习,或该窗口外的一个. samples, (中文(简体) ), zones 财务报告和审定财务报表 route 默认为假并必须读取 true 或者说 false (以四百计),当物有所收获的时候 samples, (中文(简体) ), heart_rate_zones 或者说 route (来自种子, [] (没有) 其用户未在客户端注册的代币在两者上都可获得403. 物品总是携带 id, (中文(简体) ), upload_time, (中文(简体) ), polar_user, (中文(简体) ), device, (中文(简体) ), sport, (中文(简体) ), start_time, (中文(简体) ), start_time_utc_offset 财务报告和审定财务报表 duration; 省略的种子 device, (中文(简体) ), device_id, (中文(简体) ), sport, (中文(简体) ), detailed_sport_info, (中文(简体) ), start_time_utc_offset, (中文(简体) ), duration, (中文(简体) ), calories, (中文(简体) ), distance, (中文(简体) ), heart_rate 或者说 has_route 获得合成默认值(一个极地范特克V,运行30分钟).
每个API路由都以401来回答一个缺失,未知或被撤销的代币.
测试控制
通过选项进行种子( E)clients, (中文(简体) ), users, (中文(简体) ), registrations, (中文(简体) ), exercises, (中文(简体) ), tokens, (中文(简体) ), codes, (中文(简体) ), settings)或下面的管理员路线;所有状态显示 GET /__admin/state. 管理路线(可与管理前缀相对):
GET|PUT /__admin/settings数字 :{consent: "approve" | "deny" | "error", consentUser: <x_user_id> | null}。 。 。 .deny重定向为error=access_denied, (中文(简体) ),error与error=server_error,两者携带state。 。 。 .POST /__admin/users{xUserId, firstName?, lastName?, birthdate?, gender?, weight?, height?}。 。 。 .POST /__admin/registrations{xUserId, memberId, registeredAt, clientId?}(ISO 8601) (英语).POST /__admin/exercises{xUserId, id, upload_time, start_time, ...fields}种子在注册前上传了1个种子,超过30天就会被排除在外.POST /__admin/tokens/revoke{token}:从那时起,这个符号回答401.- 钟( C)
POST /__admin/clock)移动30天窗口和代码寿命.
预设POST /__admin/faults {"preset": ..., "count": n}: unauthorized (401个), rate_limited (429处) RateLimit-Usage, (中文(简体) ), RateLimit-Limit, (中文(简体) ), RateLimit-Reset,没有 Retry-After, (中文(简体) ), rate_limited_retry_after (同样的加法) Retry-After: 5, (中文(简体) ), server_error (500个)和 connection_drop普通断层加任何其他状态、标题或间隔。 标题命名空间( E)x-mockingbird-namespace, (中文(简体) ), /__admin/ns/<name> 路径和无记名绘图PUT /__admin/credentials隔离用户、注册、练习和标志。 日记记录元数据,不是机构. 没有网友.
甲骨文和测试
来源于 AccessLink 引用 以及消费者的电线形状(问题546). 没有使用现场证明;这里没有任何证据可以核实. 以下为最佳猜测,而非甲骨文验证: invalid_request 错误 grant_type=refresh_token; 403用于未注册用户的练习电话; 200和 [] 用于未练习的注册用户; 404 用于未知练习 id; API 错误机构的 JSON 形状({"error": {"timestamp", "status", "errorType", "message"}}; 基于方向调整 invalid_scope, (中文(简体) ), unsupported_response_type 财务报告和审定财务报表 server_error 同意页; 十分钟代码寿命; 省略 redirect_uri 在接受的符号终点 RateLimit-* 429个预设值(记录的默认限额为每15分钟500个,每24小时5000个,已用尽)和否 Retry-After 默认值429(rate_limited_retry_after 是变体). 成功的对策永远没有 RateLimit-* 头.
bun test 运行接受测试(请求的每一个编号行为,通过 test/consumer.ts),全操作自平(同意页为浏览器重定向并被从随机行走中排除)和分差检测. bun run parity 需求 POLAR_ACCESS_TOKEN ,并进行只读操作列表信封检查而不打印数据.
故意不模拟
被贬值的交易API(创建交易,承诺),日常活动,连续心率,心电荷载,睡眠,夜充,SleepWise和Elixir数据,FIT/TCX/GPX下载,webhooks,用户删除和v4动态API. 并且: 真正的登录或同意 UI(同意页从 consent 设定、极地流动账户和设备、费率限额核算( RateLimit-* 标题只出现在强迫的429s上 training_load, (中文(简体) ), fat_percentage- 样式可选练习场(必要时种子;经过),和标志过期(expires_in 是一个常数).
API 密码
根运行时间导出 : PolarAPI, (中文(简体) ), POLAR_NAMESPACE, (中文(简体) ), POLAR_SCOPE, (中文(简体) ), EXPIRES_IN, (中文(简体) ), EXERCISE_WINDOW_MS, (中文(简体) ), CODE_TTL_MS, (中文(简体) ), DEFAULT_SETTINGS, (中文(简体) ), createRuntime, (中文(简体) ), POLAR_PRESETS, (中文(简体) ), document, (中文(简体) ), operationIds, (中文(简体) ), supportedOperationIds。类型 : PolarAPIOptions, (中文(简体) ), Client, (中文(简体) ), User, (中文(简体) ), RegistrationSeed, (中文(简体) ), Registration, (中文(简体) ), TokenGrant, (中文(简体) ), OAuthCode, (中文(简体) ), ExerciseSeed, (中文(简体) ), ExerciseRow, (中文(简体) ), ConsentMode, (中文(简体) ), Settings, (中文(简体) ), PolarRuntimeOptions, (中文(简体) ), PolarRuntime, (中文(简体) ), OperationId, (中文(简体) ), SupportedOperationId。 。 。 .
/server数字 : createServer, (中文(简体) ), serveTarget, (中文(简体) ), DEFAULT_PORT; 类型 PolarServerOptions, (中文(简体) ), PolarServer。 。 。 .