refactor: share teams drive upload flow

This commit is contained in:
Peter Steinberger 2026-03-13 16:37:50 +00:00
parent e94ac57f80
commit 6b04ab1e35
2 changed files with 157 additions and 68 deletions

View File

@ -0,0 +1,101 @@
import { describe, expect, it, vi } from "vitest";
import { uploadToOneDrive, uploadToSharePoint } from "./graph-upload.js";
describe("graph upload helpers", () => {
const tokenProvider = {
getAccessToken: vi.fn(async () => "graph-token"),
};
it("uploads to OneDrive with the personal drive path", async () => {
const fetchFn = vi.fn(
async () =>
new Response(
JSON.stringify({ id: "item-1", webUrl: "https://example.com/1", name: "a.txt" }),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
const result = await uploadToOneDrive({
buffer: Buffer.from("hello"),
filename: "a.txt",
tokenProvider,
fetchFn: fetchFn as typeof fetch,
});
expect(fetchFn).toHaveBeenCalledWith(
"https://graph.microsoft.com/v1.0/me/drive/root:/OpenClawShared/a.txt:/content",
expect.objectContaining({
method: "PUT",
headers: expect.objectContaining({
Authorization: "Bearer graph-token",
"Content-Type": "application/octet-stream",
}),
}),
);
expect(result).toEqual({
id: "item-1",
webUrl: "https://example.com/1",
name: "a.txt",
});
});
it("uploads to SharePoint with the site drive path", async () => {
const fetchFn = vi.fn(
async () =>
new Response(
JSON.stringify({ id: "item-2", webUrl: "https://example.com/2", name: "b.txt" }),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
const result = await uploadToSharePoint({
buffer: Buffer.from("world"),
filename: "b.txt",
siteId: "site-123",
tokenProvider,
fetchFn: fetchFn as typeof fetch,
});
expect(fetchFn).toHaveBeenCalledWith(
"https://graph.microsoft.com/v1.0/sites/site-123/drive/root:/OpenClawShared/b.txt:/content",
expect.objectContaining({
method: "PUT",
headers: expect.objectContaining({
Authorization: "Bearer graph-token",
"Content-Type": "application/octet-stream",
}),
}),
);
expect(result).toEqual({
id: "item-2",
webUrl: "https://example.com/2",
name: "b.txt",
});
});
it("rejects upload responses missing required fields", async () => {
const fetchFn = vi.fn(
async () =>
new Response(JSON.stringify({ id: "item-3" }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
await expect(
uploadToSharePoint({
buffer: Buffer.from("world"),
filename: "bad.txt",
siteId: "site-123",
tokenProvider,
fetchFn: fetchFn as typeof fetch,
}),
).rejects.toThrow("SharePoint upload response missing required fields");
});
});

View File

@ -21,6 +21,53 @@ export interface OneDriveUploadResult {
name: string; name: string;
} }
function parseUploadedDriveItem(
data: { id?: string; webUrl?: string; name?: string },
label: "OneDrive" | "SharePoint",
): OneDriveUploadResult {
if (!data.id || !data.webUrl || !data.name) {
throw new Error(`${label} upload response missing required fields`);
}
return {
id: data.id,
webUrl: data.webUrl,
name: data.name,
};
}
async function uploadDriveItem(params: {
buffer: Buffer;
filename: string;
contentType?: string;
tokenProvider: MSTeamsAccessTokenProvider;
fetchFn?: typeof fetch;
url: string;
label: "OneDrive" | "SharePoint";
}): Promise<OneDriveUploadResult> {
const fetchFn = params.fetchFn ?? fetch;
const token = await params.tokenProvider.getAccessToken(GRAPH_SCOPE);
const res = await fetchFn(params.url, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": params.contentType ?? "application/octet-stream",
},
body: new Uint8Array(params.buffer),
});
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(`${params.label} upload failed: ${res.status} ${res.statusText} - ${body}`);
}
return parseUploadedDriveItem(
(await res.json()) as { id?: string; webUrl?: string; name?: string },
params.label,
);
}
/** /**
* Upload a file to the user's OneDrive root folder. * Upload a file to the user's OneDrive root folder.
* For larger files, this uses the simple upload endpoint (up to 4MB). * For larger files, this uses the simple upload endpoint (up to 4MB).
@ -32,41 +79,13 @@ export async function uploadToOneDrive(params: {
tokenProvider: MSTeamsAccessTokenProvider; tokenProvider: MSTeamsAccessTokenProvider;
fetchFn?: typeof fetch; fetchFn?: typeof fetch;
}): Promise<OneDriveUploadResult> { }): Promise<OneDriveUploadResult> {
const fetchFn = params.fetchFn ?? fetch;
const token = await params.tokenProvider.getAccessToken(GRAPH_SCOPE);
// Use "OpenClawShared" folder to organize bot-uploaded files // Use "OpenClawShared" folder to organize bot-uploaded files
const uploadPath = `/OpenClawShared/${encodeURIComponent(params.filename)}`; const uploadPath = `/OpenClawShared/${encodeURIComponent(params.filename)}`;
return await uploadDriveItem({
const res = await fetchFn(`${GRAPH_ROOT}/me/drive/root:${uploadPath}:/content`, { ...params,
method: "PUT", url: `${GRAPH_ROOT}/me/drive/root:${uploadPath}:/content`,
headers: { label: "OneDrive",
Authorization: `Bearer ${token}`,
"Content-Type": params.contentType ?? "application/octet-stream",
},
body: new Uint8Array(params.buffer),
}); });
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(`OneDrive upload failed: ${res.status} ${res.statusText} - ${body}`);
}
const data = (await res.json()) as {
id?: string;
webUrl?: string;
name?: string;
};
if (!data.id || !data.webUrl || !data.name) {
throw new Error("OneDrive upload response missing required fields");
}
return {
id: data.id,
webUrl: data.webUrl,
name: data.name,
};
} }
export interface OneDriveSharingLink { export interface OneDriveSharingLink {
@ -175,44 +194,13 @@ export async function uploadToSharePoint(params: {
siteId: string; siteId: string;
fetchFn?: typeof fetch; fetchFn?: typeof fetch;
}): Promise<OneDriveUploadResult> { }): Promise<OneDriveUploadResult> {
const fetchFn = params.fetchFn ?? fetch;
const token = await params.tokenProvider.getAccessToken(GRAPH_SCOPE);
// Use "OpenClawShared" folder to organize bot-uploaded files // Use "OpenClawShared" folder to organize bot-uploaded files
const uploadPath = `/OpenClawShared/${encodeURIComponent(params.filename)}`; const uploadPath = `/OpenClawShared/${encodeURIComponent(params.filename)}`;
return await uploadDriveItem({
const res = await fetchFn( ...params,
`${GRAPH_ROOT}/sites/${params.siteId}/drive/root:${uploadPath}:/content`, url: `${GRAPH_ROOT}/sites/${params.siteId}/drive/root:${uploadPath}:/content`,
{ label: "SharePoint",
method: "PUT", });
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": params.contentType ?? "application/octet-stream",
},
body: new Uint8Array(params.buffer),
},
);
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(`SharePoint upload failed: ${res.status} ${res.statusText} - ${body}`);
}
const data = (await res.json()) as {
id?: string;
webUrl?: string;
name?: string;
};
if (!data.id || !data.webUrl || !data.name) {
throw new Error("SharePoint upload response missing required fields");
}
return {
id: data.id,
webUrl: data.webUrl,
name: data.name,
};
} }
export interface ChatMember { export interface ChatMember {