Files
moreminimore-vibe/testing/fake-llm-server/githubHandler.ts
Kunthawat Greethong 11986a0196 Add project files
2025-12-05 09:26:53 +07:00

419 lines
12 KiB
TypeScript

import { Request, Response } from "express";
import * as path from "path";
import * as fs from "fs";
import * as os from "os";
const gitHttpMiddlewareFactory = require("git-http-mock-server/middleware");
// Push event tracking for tests
interface PushEvent {
timestamp: Date;
repo: string;
branch: string;
operation: "push" | "create" | "delete";
commitSha?: string;
}
const pushEvents: PushEvent[] = [];
// Mock data for testing
const mockAccessToken = "fake_access_token_12345";
const mockDeviceCode = "fake_device_code_12345";
const mockUserCode = "FAKE-CODE";
const mockUser = {
login: "testuser",
id: 12345,
email: "testuser@example.com",
};
const mockRepos = [
{
id: 1,
name: "test-repo-1",
full_name: "testuser/test-repo-1",
private: false,
owner: { login: "testuser" },
default_branch: "main",
},
{
id: 2,
name: "test-repo-2",
full_name: "testuser/test-repo-2",
private: true,
owner: { login: "testuser" },
default_branch: "main",
},
{
id: 3,
name: "existing-app",
full_name: "testuser/existing-app",
private: false,
owner: { login: "testuser" },
default_branch: "main",
},
];
const mockBranches = [
{ name: "main", commit: { sha: "abc123" } },
{ name: "develop", commit: { sha: "def456" } },
{ name: "feature/test", commit: { sha: "ghi789" } },
];
// Store device flow state
let deviceFlowState = {
deviceCode: mockDeviceCode,
userCode: mockUserCode,
authorized: false,
pollCount: 0,
};
// GitHub Device Flow - Step 1: Get device code
export function handleDeviceCode(req: Request, res: Response) {
console.log("* GitHub Device Code requested");
// Reset state for new flow
deviceFlowState = {
deviceCode: mockDeviceCode,
userCode: mockUserCode,
authorized: false,
pollCount: 0,
};
res.json({
device_code: mockDeviceCode,
user_code: mockUserCode,
verification_uri: "https://github.com/login/device",
verification_uri_complete: `https://github.com/login/device?user_code=${mockUserCode}`,
expires_in: 900,
interval: 1, // Short interval for testing
});
}
// GitHub Device Flow - Step 2: Poll for access token
export function handleAccessToken(req: Request, res: Response) {
console.log("* GitHub Access Token polling", {
pollCount: deviceFlowState.pollCount,
});
const { device_code } = req.body;
if (device_code !== mockDeviceCode) {
return res.status(400).json({
error: "invalid_request",
error_description: "Invalid device code",
});
}
deviceFlowState.pollCount++;
// Simulate authorization after 3 polls (for testing)
if (deviceFlowState.pollCount >= 3) {
deviceFlowState.authorized = true;
return res.json({
access_token: mockAccessToken,
token_type: "bearer",
scope: "repo,user,workflow",
});
}
// Return pending status
res.status(400).json({
error: "authorization_pending",
error_description: "The authorization request is still pending",
});
}
// Get authenticated user info
export function handleUser(req: Request, res: Response) {
console.log("* GitHub User info requested");
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
res.json(mockUser);
}
// Get user emails
export function handleUserEmails(req: Request, res: Response) {
console.log("* GitHub User emails requested");
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
res.json([
{
email: "testuser@example.com",
primary: true,
verified: true,
visibility: "public",
},
]);
}
// List user repositories
export function handleUserRepos(req: Request, res: Response) {
console.log("* GitHub User repos requested");
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
if (req.method === "GET") {
// List repos
res.json(mockRepos);
} else if (req.method === "POST") {
// Create repo
const { name, private: isPrivate } = req.body;
console.log("* Creating repository:", name);
// Check if repo already exists
const existingRepo = mockRepos.find((repo) => repo.name === name);
if (existingRepo) {
return res.status(422).json({
message: "Repository creation failed.",
errors: [
{
resource: "Repository",
code: "already_exists",
field: "name",
},
],
});
}
// Create new repo
const newRepo = {
id: mockRepos.length + 1,
name,
full_name: `${mockUser.login}/${name}`,
private: !!isPrivate,
owner: { login: mockUser.login },
default_branch: "main",
};
res.status(201).json(newRepo);
}
}
// Get repository info
export function handleRepo(req: Request, res: Response) {
console.log("* GitHub Repo info requested");
const { owner, repo } = req.params;
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
const foundRepo = mockRepos.find((r) => r.full_name === `${owner}/${repo}`);
if (!foundRepo) {
return res.status(404).json({
message: "Not Found",
});
}
res.json(foundRepo);
}
// Get repository branches
export function handleRepoBranches(req: Request, res: Response) {
console.log("* GitHub Repo branches requested");
const { owner, repo } = req.params;
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
const foundRepo = mockRepos.find((r) => r.full_name === `${owner}/${repo}`);
if (!foundRepo) {
return res.status(404).json({
message: "Not Found",
});
}
res.json(mockBranches);
}
// Create repository for organization (not implemented in mock)
export function handleOrgRepos(req: Request, res: Response) {
console.log("* GitHub Org repos requested");
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
// For simplicity, just redirect to user repos for mock
handleUserRepos(req, res);
}
// Push event management functions for testing
export function handleGetPushEvents(req: Request, res: Response) {
console.log("* Getting push events");
const { repo } = req.query;
const events = repo ? pushEvents.filter((e) => e.repo === repo) : pushEvents;
res.json(events);
}
export function handleClearPushEvents(req: Request, res: Response) {
console.log("* Clearing push events");
pushEvents.length = 0;
res.json({ cleared: true, timestamp: new Date() });
}
// Handle Git operations (push, pull, clone, etc.) using git-http-mock-server
export function handleGitPush(req: Request, res: Response, next?: Function) {
console.log("* GitHub Git operation requested:", req.method, req.url);
// Log request headers to see git operation details
console.log("* Git Headers:", {
"git-protocol": req.headers["git-protocol"],
"content-type": req.headers["content-type"],
"user-agent": req.headers["user-agent"],
});
// Create a unique temporary directory for this request
const mockReposRoot = fs.mkdtempSync(
path.join(
os.tmpdir(),
"dyad-git-mock-" + Math.random().toString(36).substring(2, 15),
),
);
console.error(`* Created temporary git repos directory: ${mockReposRoot}`);
// Create git middleware instance for this request
const gitHttpMiddleware = gitHttpMiddlewareFactory({
root: mockReposRoot,
route: "/github/git",
glob: "*.git",
});
// Extract repo name from URL path like /github/git/testuser/test-repo.git
// The middleware expects the repo name as the basename after the route
const urlPath = req.url;
const match = urlPath.match(/\/github\/git\/[^/]+\/([^/.]+)\.git/);
const repoName = match?.[1];
if (repoName) {
console.log(`* Git operation for repo: ${repoName}`);
// Track push events if this is a git-receive-pack (push) operation
if (req.url.includes("/git-receive-pack") && req.method === "POST") {
console.log("* Git PUSH operation detected for repo:", repoName);
// Collect request body to parse git protocol
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
// Parse git pack protocol for branch refs
// Git protocol sends refs in format: "old-sha new-sha refs/heads/branch-name"
const lines = body.split("\n");
lines.forEach((line) => {
// Look for lines containing refs/heads/
const refMatch = line.match(
// eslint-disable-next-line
/([0-9a-f]{40})\s+([0-9a-f]{40})\s+refs\/heads\/([^\s\u0000]+)/,
);
if (refMatch) {
const [, oldSha, newSha, branchName] = refMatch;
const isDelete = newSha === "0".repeat(40);
const isCreate = oldSha === "0".repeat(40);
let operation: "push" | "create" | "delete" = "push";
if (isDelete) operation = "delete";
else if (isCreate) operation = "create";
pushEvents.push({
timestamp: new Date(),
repo: repoName,
branch: branchName,
operation,
commitSha: isDelete ? oldSha : newSha,
});
console.log(
`* Recorded ${operation} to ${repoName}/${branchName}, commit: ${isDelete ? oldSha : newSha}`,
);
}
});
} catch (error) {
console.error("* Error parsing git protocol:", error);
}
});
}
// Ensure the bare git repository exists for this repo
const bareRepoPath = path.join(mockReposRoot, `${repoName}.git`);
console.log(`* Creating bare git repository at: ${bareRepoPath}`);
try {
fs.mkdirSync(bareRepoPath, { recursive: true });
// Initialize as bare repository
const { execSync } = require("child_process");
execSync(`git init --bare`, { cwd: bareRepoPath });
console.log(
`* Successfully created bare git repository: ${repoName}.git`,
);
} catch (error) {
console.error(`* Failed to create bare git repository:`, error);
return res.status(500).json({
message: "Failed to initialize git repository",
error: error instanceof Error ? error.message : String(error),
});
}
// Rewrite the URL to match what the middleware expects
// Change /github/git/testuser/test-repo.git/... to /github/git/test-repo.git/...
const rewrittenUrl = req.url.replace(
/\/github\/git\/[^/]+\//,
"/github/git/",
);
req.url = rewrittenUrl;
console.log(`* Rewritten URL from ${urlPath} to ${rewrittenUrl}`);
}
// Use git-http-mock-server middleware to handle the actual git operations
gitHttpMiddleware(
req,
res,
next ||
(() => {
// Fallback if middleware doesn't handle the request
console.log(
`* Git middleware did not handle request: ${req.method} ${req.url}`,
);
res.status(404).json({
message: "Git operation not supported",
url: req.url,
method: req.method,
});
}),
);
}