Compare commits

...

1 Commits

Author SHA1 Message Date
Arunavo Ray
b41438f686 Release v3.2.4 - Fix metadata mirroring authentication issues
Fixed critical authentication issue causing "user does not exist [uid: 0]" errors during metadata mirroring operations. This release addresses Issue #68 and ensures proper authentication validation before all Gitea operations.

Key improvements:
- Pre-flight authentication validation for all Gitea operations
- Consistent token decryption across all API calls
- Repository existence verification before metadata operations
- Graceful fallback to user account when org creation fails
- Enhanced error messages with specific troubleshooting guidance
- Added diagnostic test scripts for authentication validation

This patch ensures metadata mirroring (issues, PRs, labels, milestones) works reliably without authentication errors.
2025-08-09 12:35:34 +05:30
9 changed files with 720 additions and 10 deletions

View File

@@ -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

View File

@@ -1,7 +1,7 @@
{
"name": "gitea-mirror",
"type": "module",
"version": "3.2.3",
"version": "3.2.4",
"engines": {
"bun": ">=1.2.9"
},

View File

@@ -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<Config>): Promise<GiteaUser> {
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<GiteaUser>(
`${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<Config>): Promise<boolean> {
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<Config>): Promise<string> {
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<Config>): 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,
};
}
}

View File

@@ -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: {

View File

@@ -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;
}

View File

@@ -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("/");

View File

@@ -47,11 +47,31 @@ export async function httpRequest<T = any>(
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,

View File

@@ -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);

View File

@@ -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);