diff --git a/CHANGELOG.md b/CHANGELOG.md index 4431a68..8583ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.2.4] - 2025-08-09 + +### Fixed +- Fixed critical authentication issue causing "user does not exist [uid: 0]" errors during metadata mirroring (Issue #68) +- Fixed inconsistent token handling across Gitea API calls +- Fixed metadata mirroring functions attempting to operate on non-existent repositories +- Fixed organization creation failing silently without proper error messages + +### Added +- Pre-flight authentication validation for all Gitea operations +- Repository existence verification before metadata mirroring +- Graceful fallback to user account when organization creation fails due to permissions +- Authentication validation utilities for debugging configuration issues +- Diagnostic test scripts for troubleshooting authentication problems + +### Improved +- Enhanced error messages with specific guidance for authentication failures +- Better identification and logging of permission-related errors +- More robust organization creation with retry logic and better error handling +- Consistent token decryption across all API operations +- Clearer error reporting for metadata mirroring failures + +### Security +- Fixed potential exposure of encrypted tokens in API calls +- Improved token handling to ensure proper decryption before use + ## [3.2.0] - 2025-07-31 ### Fixed diff --git a/package.json b/package.json index b5109de..0ce12f1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gitea-mirror", "type": "module", - "version": "3.2.3", + "version": "3.2.4", "engines": { "bun": ">=1.2.9" }, diff --git a/src/lib/gitea-auth-validator.ts b/src/lib/gitea-auth-validator.ts new file mode 100644 index 0000000..906b8f9 --- /dev/null +++ b/src/lib/gitea-auth-validator.ts @@ -0,0 +1,202 @@ +/** + * Gitea authentication and permission validation utilities + */ + +import type { Config } from "@/types/config"; +import { httpGet, HttpError } from "./http-client"; +import { decryptConfigTokens } from "./utils/config-encryption"; + +export interface GiteaUser { + id: number; + login: string; + username: string; + full_name?: string; + email?: string; + is_admin: boolean; + created?: string; + restricted?: boolean; + active?: boolean; + prohibit_login?: boolean; + location?: string; + website?: string; + description?: string; + visibility?: string; + followers_count?: number; + following_count?: number; + starred_repos_count?: number; + language?: string; +} + +/** + * Validates Gitea authentication and returns user information + */ +export async function validateGiteaAuth(config: Partial): Promise { + if (!config.giteaConfig?.url || !config.giteaConfig?.token) { + throw new Error("Gitea URL and token are required for authentication validation"); + } + + const decryptedConfig = decryptConfigTokens(config as Config); + + try { + const response = await httpGet( + `${config.giteaConfig.url}/api/v1/user`, + { + Authorization: `token ${decryptedConfig.giteaConfig.token}`, + } + ); + + const user = response.data; + + // Validate user data + if (!user.id || user.id === 0) { + throw new Error("Invalid user data received from Gitea: User ID is 0 or missing"); + } + + if (!user.username && !user.login) { + throw new Error("Invalid user data received from Gitea: Username is missing"); + } + + console.log(`[Auth Validator] Successfully authenticated as: ${user.username || user.login} (ID: ${user.id}, Admin: ${user.is_admin})`); + + return user; + } catch (error) { + if (error instanceof HttpError) { + if (error.status === 401) { + throw new Error( + "Authentication failed: The provided Gitea token is invalid or expired. " + + "Please check your Gitea configuration and ensure the token has the necessary permissions." + ); + } else if (error.status === 403) { + throw new Error( + "Permission denied: The Gitea token does not have sufficient permissions. " + + "Please ensure your token has 'read:user' scope at minimum." + ); + } + } + + throw new Error( + `Failed to validate Gitea authentication: ${error instanceof Error ? error.message : String(error)}` + ); + } +} + +/** + * Checks if the authenticated user can create organizations + */ +export async function canCreateOrganizations(config: Partial): Promise { + try { + const user = await validateGiteaAuth(config); + + // Admin users can always create organizations + if (user.is_admin) { + console.log(`[Auth Validator] User is admin, can create organizations`); + return true; + } + + // Check if the instance allows regular users to create organizations + // This would require checking instance settings, which may not be publicly available + // For now, we'll try to create a test org and see if it fails + + if (!config.giteaConfig?.url || !config.giteaConfig?.token) { + return false; + } + + const decryptedConfig = decryptConfigTokens(config as Config); + + try { + // Try to list user's organizations as a proxy for permission check + const orgsResponse = await httpGet( + `${config.giteaConfig.url}/api/v1/user/orgs`, + { + Authorization: `token ${decryptedConfig.giteaConfig.token}`, + } + ); + + // If we can list orgs, we likely can create them + console.log(`[Auth Validator] User can list organizations, likely can create them`); + return true; + } catch (listError) { + if (listError instanceof HttpError && listError.status === 403) { + console.log(`[Auth Validator] User cannot list/create organizations`); + return false; + } + // For other errors, assume we can try + return true; + } + } catch (error) { + console.error(`[Auth Validator] Error checking organization creation permissions:`, error); + return false; + } +} + +/** + * Gets or validates the default owner for repositories + */ +export async function getValidatedDefaultOwner(config: Partial): Promise { + const user = await validateGiteaAuth(config); + const username = user.username || user.login; + + if (!username) { + throw new Error("Unable to determine Gitea username from authentication"); + } + + // Check if the configured defaultOwner matches the authenticated user + if (config.giteaConfig?.defaultOwner && config.giteaConfig.defaultOwner !== username) { + console.warn( + `[Auth Validator] Configured defaultOwner (${config.giteaConfig.defaultOwner}) ` + + `does not match authenticated user (${username}). Using authenticated user.` + ); + } + + return username; +} + +/** + * Validates that the Gitea configuration is properly set up for mirroring + */ +export async function validateGiteaConfigForMirroring(config: Partial): Promise<{ + valid: boolean; + user: GiteaUser; + canCreateOrgs: boolean; + warnings: string[]; + errors: string[]; +}> { + const warnings: string[] = []; + const errors: string[] = []; + + try { + // Validate authentication + const user = await validateGiteaAuth(config); + + // Check organization creation permissions + const canCreateOrgs = await canCreateOrganizations(config); + + if (!canCreateOrgs && config.giteaConfig?.preserveOrgStructure) { + warnings.push( + "User cannot create organizations but 'preserveOrgStructure' is enabled. " + + "Repositories will be mirrored to the user account instead." + ); + } + + // Validate token scopes (this would require additional API calls to check specific permissions) + // For now, we'll just check if basic operations work + + return { + valid: true, + user, + canCreateOrgs, + warnings, + errors, + }; + } catch (error) { + errors.push(error instanceof Error ? error.message : String(error)); + + return { + valid: false, + user: {} as GiteaUser, + canCreateOrgs: false, + warnings, + errors, + }; + } +} \ No newline at end of file diff --git a/src/lib/gitea-enhanced.test.ts b/src/lib/gitea-enhanced.test.ts index a6861cd..e20f907 100644 --- a/src/lib/gitea-enhanced.test.ts +++ b/src/lib/gitea-enhanced.test.ts @@ -49,6 +49,24 @@ let createOrgCalled = false; const mockHttpGet = mock(async (url: string, headers?: any) => { // Return different responses based on URL patterns + + // Handle user authentication endpoint + if (url.includes("/api/v1/user")) { + return { + data: { + id: 1, + login: "testuser", + username: "testuser", + email: "test@example.com", + is_admin: false, + full_name: "Test User" + }, + status: 200, + statusText: "OK", + headers: new Headers() + }; + } + if (url.includes("/api/v1/repos/starred/test-repo")) { return { data: { diff --git a/src/lib/gitea-enhanced.ts b/src/lib/gitea-enhanced.ts index 48b3e60..e2a1a1a 100644 --- a/src/lib/gitea-enhanced.ts +++ b/src/lib/gitea-enhanced.ts @@ -85,6 +85,25 @@ export async function getOrCreateGiteaOrgEnhanced({ const decryptedConfig = decryptConfigTokens(config as Config); + // First, validate the user's authentication by getting their information + console.log(`[Org Creation] Validating user authentication before organization operations`); + try { + const userResponse = await httpGet( + `${config.giteaConfig.url}/api/v1/user`, + { + Authorization: `token ${decryptedConfig.giteaConfig.token}`, + } + ); + console.log(`[Org Creation] Authenticated as user: ${userResponse.data.username || userResponse.data.login} (ID: ${userResponse.data.id})`); + } catch (authError) { + if (authError instanceof HttpError && authError.status === 401) { + console.error(`[Org Creation] Authentication failed: Invalid or expired token`); + throw new Error(`Authentication failed: Please check your Gitea token has the required permissions. The token may be invalid or expired.`); + } + console.error(`[Org Creation] Failed to validate authentication:`, authError); + throw new Error(`Failed to validate Gitea authentication: ${authError instanceof Error ? authError.message : String(authError)}`); + } + for (let attempt = 0; attempt < maxRetries; attempt++) { try { console.log(`[Org Creation] Attempting to get or create organization: ${orgName} (attempt ${attempt + 1}/${maxRetries})`); @@ -164,6 +183,18 @@ export async function getOrCreateGiteaOrgEnhanced({ } continue; // Retry the loop } + + // Check for permission errors + if (createError.status === 403) { + console.error(`[Org Creation] Permission denied: User may not have rights to create organizations`); + throw new Error(`Permission denied: Your Gitea user account does not have permission to create organizations. Please ensure your account has the necessary privileges or contact your Gitea administrator.`); + } + + // Check for authentication errors + if (createError.status === 401) { + console.error(`[Org Creation] Authentication failed when creating organization`); + throw new Error(`Authentication failed: The Gitea token does not have sufficient permissions to create organizations. Please ensure your token has 'write:organization' scope.`); + } } throw createError; } diff --git a/src/lib/gitea.ts b/src/lib/gitea.ts index 54d5581..fe39c9b 100644 --- a/src/lib/gitea.ts +++ b/src/lib/gitea.ts @@ -272,7 +272,7 @@ export const mirrorGithubRepoToGitea = async ({ const decryptedConfig = decryptConfigTokens(config as Config); // Get the correct owner based on the strategy (with organization overrides) - const repoOwner = await getGiteaRepoOwnerAsync({ config, repository }); + let repoOwner = await getGiteaRepoOwnerAsync({ config, repository }); const isExisting = await isRepoPresentInGitea({ config, @@ -355,10 +355,37 @@ export const mirrorGithubRepoToGitea = async ({ // Handle organization creation if needed for single-org, preserve strategies, or starred repos if (repoOwner !== config.giteaConfig.defaultOwner) { // Need to create the organization if it doesn't exist - await getOrCreateGiteaOrg({ - orgName: repoOwner, - config, - }); + try { + await getOrCreateGiteaOrg({ + orgName: repoOwner, + config, + }); + } catch (orgError) { + console.error(`Failed to create/access organization ${repoOwner}: ${orgError instanceof Error ? orgError.message : String(orgError)}`); + + // Check if we should fallback to user account + if (orgError instanceof Error && + (orgError.message.includes('Permission denied') || + orgError.message.includes('Authentication failed') || + orgError.message.includes('does not have permission'))) { + console.warn(`[Fallback] Organization creation/access failed. Attempting to mirror to user account instead.`); + + // Update the repository owner to use the user account + repoOwner = config.giteaConfig.defaultOwner; + + // Log this fallback in the database + await db + .update(repositories) + .set({ + errorMessage: `Organization creation failed, using user account. ${orgError.message}`, + updatedAt: new Date(), + }) + .where(eq(repositories.id, repository.id!)); + } else { + // Re-throw if it's not a permission issue + throw orgError; + } + } } // Check if repository already exists as a non-mirror @@ -1064,6 +1091,19 @@ export const mirrorGitRepoIssuesToGitea = async ({ // Decrypt config tokens for API usage const decryptedConfig = decryptConfigTokens(config as Config); + + // Verify the repository exists in Gitea before attempting to mirror metadata + console.log(`[Issues] Verifying repository ${repository.name} exists at ${giteaOwner}`); + const repoExists = await isRepoPresentInGitea({ + config, + owner: giteaOwner, + repoName: repository.name, + }); + + if (!repoExists) { + console.error(`[Issues] Repository ${repository.name} not found at ${giteaOwner}. Cannot mirror issues.`); + throw new Error(`Repository ${repository.name} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`); + } const [owner, repo] = repository.fullName.split("/"); @@ -1130,7 +1170,7 @@ export const mirrorGitRepoIssuesToGitea = async ({ }/labels`, { name, color: "#ededed" }, // Default color { - Authorization: `token ${config.giteaConfig!.token}`, + Authorization: `token ${decryptedConfig.giteaConfig!.token}`, } ); @@ -1167,7 +1207,7 @@ export const mirrorGitRepoIssuesToGitea = async ({ }/issues`, issuePayload, { - Authorization: `token ${config.giteaConfig!.token}`, + Authorization: `token ${decryptedConfig.giteaConfig!.token}`, } ); @@ -1196,7 +1236,7 @@ export const mirrorGitRepoIssuesToGitea = async ({ body: `@${comment.user?.login} commented on GitHub:\n\n${comment.body}`, }, { - Authorization: `token ${config.giteaConfig!.token}`, + Authorization: `token ${decryptedConfig.giteaConfig!.token}`, } ); return comment; @@ -1312,6 +1352,19 @@ export async function mirrorGitRepoPullRequestsToGitea({ // Decrypt config tokens for API usage const decryptedConfig = decryptConfigTokens(config as Config); + + // Verify the repository exists in Gitea before attempting to mirror metadata + console.log(`[Pull Requests] Verifying repository ${repository.name} exists at ${giteaOwner}`); + const repoExists = await isRepoPresentInGitea({ + config, + owner: giteaOwner, + repoName: repository.name, + }); + + if (!repoExists) { + console.error(`[Pull Requests] Repository ${repository.name} not found at ${giteaOwner}. Cannot mirror PRs.`); + throw new Error(`Repository ${repository.name} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`); + } const [owner, repo] = repository.fullName.split("/"); @@ -1414,6 +1467,19 @@ export async function mirrorGitRepoLabelsToGitea({ // Decrypt config tokens for API usage const decryptedConfig = decryptConfigTokens(config as Config); + + // Verify the repository exists in Gitea before attempting to mirror metadata + console.log(`[Labels] Verifying repository ${repository.name} exists at ${giteaOwner}`); + const repoExists = await isRepoPresentInGitea({ + config, + owner: giteaOwner, + repoName: repository.name, + }); + + if (!repoExists) { + console.error(`[Labels] Repository ${repository.name} not found at ${giteaOwner}. Cannot mirror labels.`); + throw new Error(`Repository ${repository.name} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`); + } const [owner, repo] = repository.fullName.split("/"); @@ -1495,6 +1561,19 @@ export async function mirrorGitRepoMilestonesToGitea({ // Decrypt config tokens for API usage const decryptedConfig = decryptConfigTokens(config as Config); + + // Verify the repository exists in Gitea before attempting to mirror metadata + console.log(`[Milestones] Verifying repository ${repository.name} exists at ${giteaOwner}`); + const repoExists = await isRepoPresentInGitea({ + config, + owner: giteaOwner, + repoName: repository.name, + }); + + if (!repoExists) { + console.error(`[Milestones] Repository ${repository.name} not found at ${giteaOwner}. Cannot mirror milestones.`); + throw new Error(`Repository ${repository.name} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`); + } const [owner, repo] = repository.fullName.split("/"); diff --git a/src/lib/http-client.ts b/src/lib/http-client.ts index 3267622..15aa74c 100644 --- a/src/lib/http-client.ts +++ b/src/lib/http-client.ts @@ -47,11 +47,31 @@ export async function httpRequest( try { responseText = await responseClone.text(); if (responseText) { - errorMessage += ` - ${responseText}`; + // Try to parse as JSON for better error messages + try { + const errorData = JSON.parse(responseText); + if (errorData.message) { + errorMessage = `HTTP ${response.status}: ${errorData.message}`; + } else { + errorMessage += ` - ${responseText}`; + } + } catch { + // Not JSON, use as-is + errorMessage += ` - ${responseText}`; + } } } catch { // Ignore text parsing errors } + + // Log authentication-specific errors for debugging + if (response.status === 401) { + console.error(`[HTTP Client] Authentication failed for ${url}`); + console.error(`[HTTP Client] Response: ${responseText}`); + if (responseText.includes('user does not exist') && responseText.includes('uid: 0')) { + console.error(`[HTTP Client] Token appears to be invalid or the user account is not properly configured in Gitea`); + } + } throw new HttpError( errorMessage, diff --git a/src/tests/test-gitea-auth.ts b/src/tests/test-gitea-auth.ts new file mode 100644 index 0000000..47dc5df --- /dev/null +++ b/src/tests/test-gitea-auth.ts @@ -0,0 +1,161 @@ +#!/usr/bin/env bun + +/** + * Test script to validate Gitea authentication and permissions + * Run with: bun run src/tests/test-gitea-auth.ts + */ + +import { validateGiteaAuth, canCreateOrganizations, validateGiteaConfigForMirroring } from "@/lib/gitea-auth-validator"; +import { getConfigsByUserId } from "@/lib/db/queries/configs"; +import { db, users } from "@/lib/db"; +import { eq } from "drizzle-orm"; + +async function testGiteaAuthentication() { + console.log("=".repeat(60)); + console.log("GITEA AUTHENTICATION TEST"); + console.log("=".repeat(60)); + + try { + // Get the first user for testing + const userList = await db.select().from(users).limit(1); + + if (userList.length === 0) { + console.error("❌ No users found in database. Please set up a user first."); + process.exit(1); + } + + const user = userList[0]; + console.log(`\n✅ Found user: ${user.email} (ID: ${user.id})`); + + // Get the user's configuration + const configs = await getConfigsByUserId(user.id); + + if (configs.length === 0) { + console.error("❌ No configuration found for user. Please configure Gitea settings."); + process.exit(1); + } + + const config = configs[0]; + console.log(`✅ Found configuration (ID: ${config.id})`); + + if (!config.giteaConfig?.url || !config.giteaConfig?.token) { + console.error("❌ Gitea configuration is incomplete. URL or token is missing."); + process.exit(1); + } + + console.log(`\n📡 Testing connection to: ${config.giteaConfig.url}`); + console.log("-".repeat(60)); + + // Test 1: Validate authentication + console.log("\n🔐 Test 1: Validating authentication..."); + try { + const giteaUser = await validateGiteaAuth(config); + console.log(`✅ Authentication successful!`); + console.log(` - Username: ${giteaUser.username || giteaUser.login}`); + console.log(` - User ID: ${giteaUser.id}`); + console.log(` - Is Admin: ${giteaUser.is_admin}`); + console.log(` - Email: ${giteaUser.email || 'Not provided'}`); + } catch (error) { + console.error(`❌ Authentication failed: ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } + + // Test 2: Check organization creation permissions + console.log("\n🏢 Test 2: Checking organization creation permissions..."); + try { + const canCreate = await canCreateOrganizations(config); + if (canCreate) { + console.log(`✅ User can create organizations`); + } else { + console.log(`⚠️ User cannot create organizations (will use fallback to user account)`); + } + } catch (error) { + console.error(`❌ Error checking permissions: ${error instanceof Error ? error.message : String(error)}`); + } + + // Test 3: Full validation for mirroring + console.log("\n🔍 Test 3: Full validation for mirroring..."); + try { + const validation = await validateGiteaConfigForMirroring(config); + + if (validation.valid) { + console.log(`✅ Configuration is valid for mirroring`); + } else { + console.log(`❌ Configuration is not valid for mirroring`); + } + + if (validation.warnings.length > 0) { + console.log(`\n⚠️ Warnings:`); + validation.warnings.forEach(warning => { + console.log(` - ${warning}`); + }); + } + + if (validation.errors.length > 0) { + console.log(`\n❌ Errors:`); + validation.errors.forEach(error => { + console.log(` - ${error}`); + }); + } + } catch (error) { + console.error(`❌ Validation error: ${error instanceof Error ? error.message : String(error)}`); + } + + // Test 4: Check specific API endpoints + console.log("\n🔧 Test 4: Testing specific API endpoints..."); + + // Import HTTP client for direct API testing + const { httpGet } = await import("@/lib/http-client"); + const { decryptConfigTokens } = await import("@/lib/utils/config-encryption"); + const decryptedConfig = decryptConfigTokens(config); + + // Test organization listing + try { + const orgsResponse = await httpGet( + `${config.giteaConfig.url}/api/v1/user/orgs`, + { + Authorization: `token ${decryptedConfig.giteaConfig.token}`, + } + ); + console.log(`✅ Can list organizations (found ${orgsResponse.data.length})`); + } catch (error) { + console.log(`⚠️ Cannot list organizations: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + + // Test repository listing + try { + const reposResponse = await httpGet( + `${config.giteaConfig.url}/api/v1/user/repos`, + { + Authorization: `token ${decryptedConfig.giteaConfig.token}`, + } + ); + console.log(`✅ Can list repositories (found ${reposResponse.data.length})`); + } catch (error) { + console.error(`❌ Cannot list repositories: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + + console.log("\n" + "=".repeat(60)); + console.log("TEST COMPLETE"); + console.log("=".repeat(60)); + + // Summary + console.log("\n📊 Summary:"); + console.log(` - Gitea URL: ${config.giteaConfig.url}`); + console.log(` - Default Owner: ${config.giteaConfig.defaultOwner || 'Not set'}`); + console.log(` - Mirror Strategy: ${config.githubConfig?.mirrorStrategy || 'Not set'}`); + console.log(` - Organization: ${config.giteaConfig.organization || 'Not set'}`); + console.log(` - Preserve Org Structure: ${config.giteaConfig.preserveOrgStructure || false}`); + + console.log("\n✨ All tests completed successfully!"); + + } catch (error) { + console.error("\n❌ Test failed with error:", error); + process.exit(1); + } + + process.exit(0); +} + +// Run the test +testGiteaAuthentication().catch(console.error); \ No newline at end of file diff --git a/src/tests/test-metadata-mirroring.ts b/src/tests/test-metadata-mirroring.ts new file mode 100644 index 0000000..0074528 --- /dev/null +++ b/src/tests/test-metadata-mirroring.ts @@ -0,0 +1,173 @@ +#!/usr/bin/env bun + +/** + * Test script to verify metadata mirroring authentication works correctly + * This tests the fix for issue #68 - "user does not exist [uid: 0, name: ]" + * Run with: bun run src/tests/test-metadata-mirroring.ts + */ + +import { mirrorGitRepoIssuesToGitea } from "@/lib/gitea"; +import { validateGiteaAuth } from "@/lib/gitea-auth-validator"; +import { getConfigsByUserId } from "@/lib/db/queries/configs"; +import { db, users, repositories } from "@/lib/db"; +import { eq } from "drizzle-orm"; +import { Octokit } from "@octokit/rest"; +import type { Repository } from "@/lib/db/schema"; + +async function testMetadataMirroringAuth() { + console.log("=".repeat(60)); + console.log("METADATA MIRRORING AUTHENTICATION TEST"); + console.log("=".repeat(60)); + + try { + // Get the first user for testing + const userList = await db.select().from(users).limit(1); + + if (userList.length === 0) { + console.error("❌ No users found in database. Please set up a user first."); + process.exit(1); + } + + const user = userList[0]; + console.log(`\n✅ Found user: ${user.email} (ID: ${user.id})`); + + // Get the user's configuration + const configs = await getConfigsByUserId(user.id); + + if (configs.length === 0) { + console.error("❌ No configuration found for user. Please configure GitHub and Gitea settings."); + process.exit(1); + } + + const config = configs[0]; + console.log(`✅ Found configuration (ID: ${config.id})`); + + if (!config.giteaConfig?.url || !config.giteaConfig?.token) { + console.error("❌ Gitea configuration is incomplete. URL or token is missing."); + process.exit(1); + } + + if (!config.githubConfig?.token) { + console.error("❌ GitHub configuration is incomplete. Token is missing."); + process.exit(1); + } + + console.log(`\n📡 Testing Gitea connection to: ${config.giteaConfig.url}`); + console.log("-".repeat(60)); + + // Test 1: Validate Gitea authentication + console.log("\n🔐 Test 1: Validating Gitea authentication..."); + let giteaUser; + try { + giteaUser = await validateGiteaAuth(config); + console.log(`✅ Gitea authentication successful!`); + console.log(` - Username: ${giteaUser.username || giteaUser.login}`); + console.log(` - User ID: ${giteaUser.id}`); + console.log(` - Is Admin: ${giteaUser.is_admin}`); + } catch (error) { + console.error(`❌ Gitea authentication failed: ${error instanceof Error ? error.message : String(error)}`); + console.error(` This is the root cause of the "user does not exist [uid: 0]" error`); + process.exit(1); + } + + // Test 2: Check if we can access a test repository + console.log("\n📦 Test 2: Looking for a test repository..."); + + // Get a repository from the database + const repos = await db.select().from(repositories) + .where(eq(repositories.userId, user.id)) + .limit(1); + + if (repos.length === 0) { + console.log("⚠️ No repositories found in database. Skipping metadata mirroring test."); + console.log(" Please run a mirror operation first to test metadata mirroring."); + } else { + const testRepo = repos[0] as Repository; + console.log(`✅ Found test repository: ${testRepo.fullName}`); + + // Test 3: Verify repository exists in Gitea + console.log("\n🔍 Test 3: Verifying repository exists in Gitea..."); + + const { isRepoPresentInGitea } = await import("@/lib/gitea"); + const giteaOwner = giteaUser.username || giteaUser.login; + + const repoExists = await isRepoPresentInGitea({ + config, + owner: giteaOwner, + repoName: testRepo.name, + }); + + if (!repoExists) { + console.log(`⚠️ Repository ${testRepo.name} not found in Gitea at ${giteaOwner}`); + console.log(` This would cause metadata mirroring to fail with authentication errors`); + console.log(` Please ensure the repository is mirrored first before attempting metadata sync`); + } else { + console.log(`✅ Repository exists in Gitea at ${giteaOwner}/${testRepo.name}`); + + // Test 4: Attempt to mirror metadata (dry run) + console.log("\n🔄 Test 4: Testing metadata mirroring authentication..."); + + try { + // Create Octokit instance + const octokit = new Octokit({ + auth: config.githubConfig.token, + }); + + // Test by attempting to fetch labels (lightweight operation) + const { httpGet } = await import("@/lib/http-client"); + const { decryptConfigTokens } = await import("@/lib/utils/config-encryption"); + const decryptedConfig = decryptConfigTokens(config); + + const labelsResponse = await httpGet( + `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${testRepo.name}/labels`, + { + Authorization: `token ${decryptedConfig.giteaConfig.token}`, + } + ); + + console.log(`✅ Successfully authenticated for metadata operations`); + console.log(` - Can access repository labels endpoint`); + console.log(` - Found ${labelsResponse.data.length} existing labels`); + console.log(` - Authentication token is valid and has proper permissions`); + + } catch (error) { + if (error instanceof Error && error.message.includes('uid: 0')) { + console.error(`❌ CRITICAL: Authentication failed with "uid: 0" error!`); + console.error(` This is the exact issue from GitHub issue #68`); + console.error(` Error: ${error.message}`); + } else { + console.error(`❌ Metadata operation failed: ${error instanceof Error ? error.message : String(error)}`); + } + } + } + } + + console.log("\n" + "=".repeat(60)); + console.log("TEST COMPLETE"); + console.log("=".repeat(60)); + + // Summary + console.log("\n📊 Summary:"); + console.log(` - Gitea URL: ${config.giteaConfig.url}`); + console.log(` - Gitea User: ${giteaUser?.username || giteaUser?.login || 'Unknown'}`); + console.log(` - Authentication: ${giteaUser ? '✅ Valid' : '❌ Invalid'}`); + console.log(` - Metadata Mirroring: ${config.giteaConfig.mirrorMetadata ? 'Enabled' : 'Disabled'}`); + if (config.giteaConfig.mirrorMetadata) { + console.log(` - Issues: ${config.giteaConfig.mirrorIssues ? 'Yes' : 'No'}`); + console.log(` - Pull Requests: ${config.giteaConfig.mirrorPullRequests ? 'Yes' : 'No'}`); + console.log(` - Labels: ${config.giteaConfig.mirrorLabels ? 'Yes' : 'No'}`); + console.log(` - Milestones: ${config.giteaConfig.mirrorMilestones ? 'Yes' : 'No'}`); + } + + console.log("\n✨ If all tests passed, metadata mirroring should work without uid:0 errors!"); + + } catch (error) { + console.error("\n❌ Test failed with error:", error); + process.exit(1); + } + + process.exit(0); +} + +// Run the test +testMetadataMirroringAuth().catch(console.error); \ No newline at end of file