Skip to content
Open
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
199 changes: 199 additions & 0 deletions apps/mesh/src/shared/github-clone-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import {
describe,
it,
expect,
vi,
mock,
beforeAll,
afterAll,
beforeEach,
afterEach,
} from "bun:test";
import {
createTestDatabase,
closeTestDatabase,
type TestDatabase,
} from "../database/test-db";
import {
createTestSchema,
seedCommonTestFixtures,
} from "../storage/test-helpers";
import { CredentialVault } from "../encryption/credential-vault";
import { DownstreamTokenStorage } from "../storage/downstream-token";
import { ConnectionStorage } from "../storage/connection";
import type { TokenRefreshResult } from "@/oauth/refresh-access-token";

const mockRefreshAccessToken =
vi.fn<
(
...args: Parameters<
typeof import("@/oauth/refresh-access-token").refreshAccessToken
>
) => Promise<TokenRefreshResult>
>();
mock.module("@/oauth/refresh-access-token", () => ({
refreshAccessToken: mockRefreshAccessToken,
}));

const { buildCloneInfo } = await import("./github-clone-info");

describe("buildCloneInfo", () => {
let database: TestDatabase;
let vault: CredentialVault;
let tokenStorage: DownstreamTokenStorage;
const connectionId = "conn_github_clone_test";
const originalFetch = globalThis.fetch;
let fetchCalls: string[] = [];

beforeAll(async () => {
database = await createTestDatabase();
await createTestSchema(database.db);
await seedCommonTestFixtures(database.db);

vault = new CredentialVault(CredentialVault.generateKey());
tokenStorage = new DownstreamTokenStorage(database.db, vault);

const connectionStorage = new ConnectionStorage(database.db, vault);
await connectionStorage.create({
id: connectionId,
organization_id: "org_123",
created_by: "user_1",
title: "GitHub",
connection_type: "HTTP",
connection_url: "https://mcp.example.com/github",
connection_token: null,
tools: null,
});
});

afterAll(async () => {
await closeTestDatabase(database);
globalThis.fetch = originalFetch;
});

beforeEach(async () => {
fetchCalls = [];
mockRefreshAccessToken.mockReset();
await tokenStorage.delete(connectionId);
globalThis.fetch = (async (
input: RequestInfo | URL,
_init?: RequestInit,
) => {
const url = typeof input === "string" ? input : input.toString();
fetchCalls.push(url);
throw new Error(`buildCloneInfo must not fetch — got ${url}`);
}) as unknown as typeof globalThis.fetch;
});

afterEach(() => {
globalThis.fetch = originalFetch;
});

it("returns clone URL + bot identity without making any GitHub API call", async () => {
await tokenStorage.upsert({
connectionId,
accessToken: "install-token-abc",
refreshToken: "rt",
scope: "repo",
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
clientId: "cid",
clientSecret: "csecret",
tokenEndpoint: "https://github.com/login/oauth/access_token",
});

const info = await buildCloneInfo(
connectionId,
"octocat",
"hello-world",
database.db,
vault,
);

expect(info.cloneUrl).toBe(
"https://x-access-token:install-token-abc@github.com/octocat/hello-world.git",
);
expect(info.gitUserName).toBe("mcp-github[bot]");
expect(info.gitUserEmail).toBe("mcp-github[bot]@users.noreply.github.com");
expect(fetchCalls).toEqual([]);
expect(mockRefreshAccessToken).not.toHaveBeenCalled();
});

it("proactively refreshes an expired token and embeds the fresh one", async () => {
await tokenStorage.upsert({
connectionId,
accessToken: "stale-token",
refreshToken: "rt",
scope: "repo",
expiresAt: new Date(Date.now() - 60 * 1000),
clientId: "cid",
clientSecret: "csecret",
tokenEndpoint: "https://github.com/login/oauth/access_token",
});

mockRefreshAccessToken.mockResolvedValueOnce({
success: true,
accessToken: "fresh-token",
refreshToken: "rt2",
expiresIn: 3600,
scope: "repo",
});

const info = await buildCloneInfo(
connectionId,
"octocat",
"hello-world",
database.db,
vault,
);

expect(info.cloneUrl).toContain("x-access-token:fresh-token@");
expect(info.gitUserName).toBe("mcp-github[bot]");
expect(info.gitUserEmail).toBe("mcp-github[bot]@users.noreply.github.com");
expect(mockRefreshAccessToken).toHaveBeenCalledTimes(1);
expect(fetchCalls).toEqual([]);
});

it("throws RECONNECT_ERROR when proactive refresh fails", async () => {
await tokenStorage.upsert({
connectionId,
accessToken: "stale-token",
refreshToken: "rt",
scope: "repo",
expiresAt: new Date(Date.now() - 60 * 1000),
clientId: "cid",
clientSecret: "csecret",
tokenEndpoint: "https://github.com/login/oauth/access_token",
});

mockRefreshAccessToken.mockResolvedValueOnce({
success: false,
error: "invalid_grant",
});

await expect(
buildCloneInfo(
connectionId,
"octocat",
"hello-world",
database.db,
vault,
),
).rejects.toThrow(/reconnect/i);

expect(fetchCalls).toEqual([]);
});

it("throws when no token is stored for the connection", async () => {
await expect(
buildCloneInfo(
connectionId,
"octocat",
"hello-world",
database.db,
vault,
),
).rejects.toThrow(/No GitHub token found/i);

expect(fetchCalls).toEqual([]);
});
});
38 changes: 11 additions & 27 deletions apps/mesh/src/shared/github-clone-info.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Authenticated clone URL + git identity from a connection's OAuth token.
* Falls back to generic defaults on /user failure so callers never block
* on a flaky upstream.
* Authenticated clone URL + bot git identity from a connection's downstream
* App installation token. Makes no GitHub API call — the committer is the
* Mesh GitHub App bot.
*/

import type { Kysely } from "kysely";
Expand All @@ -15,6 +15,9 @@ import {
refreshAndStore,
} from "../oauth/token-refresh";

const MCP_GITHUB_BOT_NAME = "mcp-github[bot]";
const MCP_GITHUB_BOT_EMAIL = "mcp-github[bot]@users.noreply.github.com";

export interface GitHubCloneInfo {
cloneUrl: string;
gitUserName: string;
Expand All @@ -38,7 +41,6 @@ export async function buildCloneInfo(

let accessToken = token.accessToken;

// Proactive refresh before baking into the clone URL. Mirrors GITHUB_LIST_USER_ORGS.
if (
canRefresh(token) &&
tokenStorage.isExpired(token, PROACTIVE_REFRESH_BUFFER_MS)
Expand All @@ -52,27 +54,9 @@ export async function buildCloneInfo(

const cloneUrl = `https://x-access-token:${accessToken}@github.com/${owner}/${name}.git`;

let gitUserName = "Deco Studio";
let gitUserEmail = "studio@deco.cx";
try {
const res = await fetch("https://api.github.com/user", {
headers: {
Authorization: `token ${accessToken}`,
Accept: "application/vnd.github+json",
},
});
if (res.ok) {
const user = (await res.json()) as {
name?: string | null;
login: string;
email?: string | null;
};
gitUserName = user.name || user.login;
gitUserEmail = user.email || `${user.login}@users.noreply.github.com`;
}
} catch {
// Fallback to defaults — don't block the caller.
}

return { cloneUrl, gitUserName, gitUserEmail };
return {
cloneUrl,
gitUserName: MCP_GITHUB_BOT_NAME,
gitUserEmail: MCP_GITHUB_BOT_EMAIL,
};
}
Loading
Loading