Set up a local development loop for OneNote integrations with mock Graph API responses.
Use when developing OneNote features without Azure credentials or to avoid rate limits during development.
Trigger with "onenote local dev", "onenote mock", "onenote testing setup".
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Set up a local development loop for OneNote integrations with mock Graph API responses.
Use when developing OneNote features without Azure credentials or to avoid rate limits during development.
Trigger with "onenote local dev", "onenote mock", "onenote testing setup".
allowed-tools
Read, Write, Edit, Bash(npm:*), Bash(pip:*), Grep
version
1.6.0
license
MIT
author
Jeremy Longshore <jeremy@intentsolutions.io>
tags
["saas","onenote","microsoft"]
compatibility
Designed for Claude Code
OneNote Local Dev Loop
Overview
Testing OneNote integrations typically requires Azure AD credentials and live Graph API calls, which means authentication friction on every dev session and risk of hitting the 600 req/60s rate limit during rapid iteration. This skill sets up a local development loop with mock Graph responses so you can develop and test OneNote features without Azure credentials, without rate limits, and with instant feedback.
The mock layer intercepts HTTP calls to graph.microsoft.com and returns realistic fixture data, including the XHTML output format that differs from input format. You can switch between mock and live Graph with a single environment variable.
Prerequisites
Node.js 18+ or Python 3.10+
Familiarity with your project's test framework (vitest/jest for Node, pytest for Python)
Optional: completed onenote-install-auth for live mode switching
<!-- tests/fixtures/page-content.html --><!-- NOTE: This is OUTPUT format — Graph normalizes your input HTML --><!-- Output includes data-id attributes, absolute positioning, div wrappers --><htmllang="en-US"><head><title>Sprint Planning Notes</title><metahttp-equiv="Content-Type"content="text/html; charset=utf-8" /></head><bodydata-absolute-enabled="true"style="font-family:Calibri;font-size:11pt"><divid="div-{guid}"data-id="div1"style="position:absolute;left:48px;top:115px;width:624px"><h1style="font-size:16pt;color:#1e4e79;margin-top:11pt;margin-bottom:11pt">
Sprint Planning Notes
</h1><pdata-id="p1">Attendees: Alice, Bob, Charlie</p><h2style="font-size:14pt;color:#2e74b5;margin-top:11pt;margin-bottom:11pt">
Action Items
</h2><ul><lidata-id="li1"data-tag="to-do"style="--tag-state:unchecked">Deploy feature X by Friday</li><lidata-id="li2"data-tag="to-do"style="--tag-state:unchecked">Review PR #488</li></ul></div></body></html>
Step 5: Environment Switching (Mock vs Live)
// src/client.tsimport { Client } from"@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from"@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
import { DeviceCodeCredential } from"@azure/identity";
exportfunctioncreateGraphClient(): Client {
const mode = process.env.GRAPH_MODE ?? "mock";
if (mode === "live") {
const credential = newDeviceCodeCredential({
clientId: process.env.AZURE_CLIENT_ID!,
tenantId: process.env.AZURE_TENANT_ID!,
});
const authProvider = newTokenCredentialAuthenticationProvider(credential, {
scopes: ["Notes.ReadWrite"],
});
returnClient.initWithMiddleware({ authProvider });
}
// In mock mode, MSW intercepts all requests — no auth needed// Use a dummy auth provider that returns a fake tokenreturnClient.init({
authProvider: (done) =>done(null, "mock-token-for-dev"),
});
}
# .env.example (commit this file)# Set GRAPH_MODE=mock for local development (no Azure credentials needed)# Set GRAPH_MODE=live to use real Graph API (requires AZURE_CLIENT_ID and AZURE_TENANT_ID)
GRAPH_MODE=mock
AZURE_CLIENT_ID=
AZURE_TENANT_ID=
Step 6: Python Mock Setup (responses library)
# tests/conftest.py — Python mock setup using responses libraryimport json, pytest, responses
from pathlib import Path
FIXTURES = Path(__file__).parent / "fixtures"
BASE = "https://graph.microsoft.com/v1.0"@pytest.fixturedefmock_graph():
"""Activate mock Graph API responses for all tests."""with responses.RequestsMock() as rsps:
rsps.add(responses.GET, f"{BASE}/me/onenote/notebooks",
json=json.loads((FIXTURES / "notebooks.json").read_text()), status=200)
rsps.add_callback(responses.POST, f"{BASE}/me/onenote/notebooks",
callback=lambda req: (201, {}, json.dumps({
"id": f"nb-{hash(req.body) % 10000}",
"displayName": json.loads(req.body)["displayName"]})))
yield rsps
Step 7: Test Isolation Patterns
// tests/onenote.test.tsimport { describe, it, expect } from"vitest";
import { createGraphClient } from"../src/client";
import { mockServer } from"./setup";
import { http, HttpResponse } from"msw";
describe("OneNote integration", () => {
const client = createGraphClient();
it("lists notebooks", async () => {
const response = await client.api("/me/onenote/notebooks").get();
expect(response.value).toHaveLength(2);
expect(response.value[0].displayName).toBe("Work Notes");
});
it("creates a page with valid XHTML", async () => {
const xhtml = `<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Test Page</title></head>
<body><p>Hello World</p></body>
</html>`;
const page = await client
.api("/me/onenote/sections/section-abc/pages")
.header("Content-Type", "text/html")
.post(xhtml);
expect(page.id).toBeDefined();
expect(page.title).toBe("Test Page");
});
it("handles 429 rate limit", async () => {
// Override handler for this test only
mockServer.use(
http.get(
"https://graph.microsoft.com/v1.0/me/onenote/notebooks",
() => {
returnnewHttpResponse(
JSON.stringify({ error: { code: "429", message: "Throttled" } }),
{ status: 429, headers: { "Retry-After": "1" } }
);
},
{ once: true } // Only intercept once, then fall through to default
)
);
// Your retry logic should handle this and succeed on second attempt
});
it("detects silent upload failure", async () => {
// Override to return empty body (simulates >4MB upload)
mockServer.use(
http.post(
"https://graph.microsoft.com/v1.0/me/onenote/sections/:sectionId/pages",
() => {
returnHttpResponse.json(null, { status: 200 });
},
{ once: true }
)
);
const response = await client
.api("/me/onenote/sections/section-abc/pages")
.header("Content-Type", "text/html")
.post("<html><head><title>Big</title></head><body>...</body></html>");
// This is the silent failure — 200 but no idexpect(response?.id).toBeUndefined();
});
});