diff --git a/main.js b/main.js index 3fe7cb4..d47902b 100644 --- a/main.js +++ b/main.js @@ -10,7 +10,7 @@ import { load } from 'js-yaml'; import { initializeUIFromConfig, setThemeToggle } from './src/ui/initUserInterface.js'; import { parseUrlParams, updateUrlParams, getCurrentState } from './src/ui/urlManager.js'; import { renderItemList } from './src/ui/render.js'; -import { getPlatformApiUrls } from './src/utils/defineApiUrls.js'; +import { getPlatformApiUrls } from './src/utils/definePlatformVals.js'; import { filterItems, sortItems } from './src/utils/filterAndSort.js'; import { fetchCodeRepos } from './src/api/fetchCodeRepos.js'; import { fetchHfRepos } from './src/api/fetchHfRepos.js'; @@ -28,7 +28,7 @@ const configPromise = fetch('config.yaml') // Module-scope lets — assigned after config loads, used by all functions below let CONFIG; let ORGANIZATION_NAME, HF_ORGANIZATION_NAME, CATALOG_REPO_NAME, PLATFORM, API_BASE_URL, REFRESH_INTERVAL_DAYS, ADDITIONAL_REPOS, ADDITIONAL_HF_REPOS; -let ORG_API_URL, REPO_API_URL; +let ORG_API_URL, REPO_API_URL, RELEASE_SUFFIX; let releasesMap = {}; @@ -206,16 +206,23 @@ document.addEventListener('DOMContentLoaded', async () => { } // Assign module-scope variables used by all functions - ORGANIZATION_NAME = CONFIG.ORGANIZATION_NAME; - HF_ORGANIZATION_NAME = CONFIG.HF_ORGANIZATION_NAME; - CATALOG_REPO_NAME = CONFIG.CATALOG_REPO_NAME; - PLATFORM = CONFIG.PLATFORM; - ORG_API_URL = getPlatformApiUrls(PLATFORM, ORGANIZATION_NAME).org; - REPO_API_URL = getPlatformApiUrls(PLATFORM, ORGANIZATION_NAME).repo; - API_BASE_URL = CONFIG.API_BASE_URL; - REFRESH_INTERVAL_DAYS = CONFIG.REFRESH_INTERVAL_DAYS; - ADDITIONAL_REPOS = CONFIG.ADDITIONAL_REPOS; - ADDITIONAL_HF_REPOS = CONFIG.ADDITIONAL_HF_REPOS; + // Destructure CONFIG into individual variables for easier access + ({ + ORGANIZATION_NAME, + HF_ORGANIZATION_NAME, + CATALOG_REPO_NAME, + PLATFORM, + API_BASE_URL, + REFRESH_INTERVAL_DAYS, + ADDITIONAL_REPOS, + ADDITIONAL_HF_REPOS + } = CONFIG); + // Destructure platform-specific API URLs from getPlatformApiUrls + ({ + org: ORG_API_URL, + repo: REPO_API_URL, + releaseSuffix: RELEASE_SUFFIX + } = getPlatformApiUrls(PLATFORM, ORGANIZATION_NAME)); // Guard: if ORGANIZATION_NAME or HF_ORGANIZATION_NAME is missing (e.g. config.yaml failed to load), // stop here — proceeding would fire requests like ?author=&full=true which @@ -289,7 +296,7 @@ document.addEventListener('DOMContentLoaded', async () => { }); // Initialize the Catalog Badge (Stars/Forks/Version) - fetchCatalogStats(REPO_API_URL, ORGANIZATION_NAME, CATALOG_REPO_NAME) + fetchCatalogStats(REPO_API_URL, ORGANIZATION_NAME, CATALOG_REPO_NAME, RELEASE_SUFFIX) // Load pre-built release data (written by scripts/fetch-releases.js at build time) releasesMap = await fetch('./releases.json') diff --git a/scripts/export-tags.js b/scripts/export-tags.js index 7ab2497..ed18d6f 100644 --- a/scripts/export-tags.js +++ b/scripts/export-tags.js @@ -17,9 +17,9 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { load } from 'js-yaml'; import { validateConfig } from '../src/validateConfig.js'; -import { getPlatformApiUrls } from '../src/utils/defineApiUrls.js'; import { getPlatformDisplay } from '../src/utils/defineRibbonVals.js'; import { filterNewAdditionalEntries } from '../src/utils/filterNewAdditionalEntries.js'; +import { getPlatformVals, getPlatformApiUrls } from '../src/utils/definePlatformVals.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -60,6 +60,7 @@ const get = async (url) => { // --------------------------------------------------------------------------- const allTags = new Set(); const { org: ORG_API_URL, repo: REPO_API_URL } = getPlatformApiUrls(PLATFORM, ORGANIZATION_NAME); +const { profileRepo, fullNameKey, forkKey } = getPlatformVals(PLATFORM); const collectCodePlatformTags = async () => { const platformDisplay = getPlatformDisplay(PLATFORM); @@ -85,8 +86,8 @@ const collectCodePlatformTags = async () => { ); const additionalRepos = additionalData.filter(Boolean); - const additionalNames = new Set(additionalRepos.map(r => r.full_name)); - const orgNonForks = allRepos.filter(r => r.name !== '.github' && !r.fork && !additionalNames.has(r.full_name)); + const additionalNames = new Set(additionalRepos.map(r => r[fullNameKey])); + const orgNonForks = allRepos.filter(r => r.name !== profileRepo && !r[forkKey] && !additionalNames.has(r[fullNameKey])); [...additionalRepos, ...orgNonForks].forEach(repo => { (repo.topics || []).forEach(t => allTags.add(t.toLowerCase())); diff --git a/scripts/fetch-releases.js b/scripts/fetch-releases.js index 933ce34..4d3f977 100644 --- a/scripts/fetch-releases.js +++ b/scripts/fetch-releases.js @@ -6,7 +6,7 @@ import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { load } from 'js-yaml'; import { validateConfig } from '../src/validateConfig.js'; -import { getPlatformApiUrls } from '../src/utils/defineApiUrls.js'; +import { getPlatformVals, getPlatformApiUrls } from '../src/utils/definePlatformVals.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -28,7 +28,8 @@ const headers = TOKEN const TWO_WEEKS_MS = 14 * 24 * 60 * 60 * 1000; // Step 1: Fetch all public org repos (paginated, same logic as main.js) -const { org: ORG_API_URL, repo: REPO_API_URL } = getPlatformApiUrls(CONFIG.PLATFORM, CONFIG.ORGANIZATION_NAME); +const { org: ORG_API_URL, repo: REPO_API_URL, releaseSuffix: RELEASE_SUFFIX } = getPlatformApiUrls(CONFIG.PLATFORM, CONFIG.ORGANIZATION_NAME); +const { profileRepo, fullNameKey, forkKey, urlKey, releasePublishedAtKey } = getPlatformVals(CONFIG.PLATFORM); let allOrgRepos = []; let nextUrl = `${ORG_API_URL}`; while (nextUrl) { @@ -47,24 +48,24 @@ while (nextUrl) { // Step 2: Collect all code repo IDs (mirrors main.js deduplication logic) const additionalRepoIds = CONFIG.ADDITIONAL_REPOS || []; const additionalRepoSet = new Set(additionalRepoIds); -const orgNonForks = allOrgRepos.filter(r => r.name !== '.github' && !r.fork && !additionalRepoSet.has(r.full_name)); +const orgNonForks = allOrgRepos.filter(r => r.name !== profileRepo && !r[forkKey] && !additionalRepoSet.has(r[fullNameKey])); const repoIds = [ ...additionalRepoIds, - ...orgNonForks.map(r => r.full_name), + ...orgNonForks.map(r => r[fullNameKey]), ]; // Step 3: Fetch latest release for each repo const releases = {}; for (const id of repoIds) { try { - const res = await fetch(`${REPO_API_URL}${id}/releases/latest`, { headers }); + const res = await fetch(`${REPO_API_URL}${id}/${RELEASE_SUFFIX}`, { headers }); if (!res.ok) { releases[id] = null; continue; } const data = await res.json(); releases[id] = { tag: data.tag_name, - url: data.html_url, - publishedAt: data.published_at, - isNew: (Date.now() - new Date(data.published_at)) < TWO_WEEKS_MS, + url: data[urlKey], + publishedAt: data[releasePublishedAtKey], + isNew: (Date.now() - new Date(data[releasePublishedAtKey])) < TWO_WEEKS_MS, }; } catch { releases[id] = null; diff --git a/src/api/fetchCodeRepos.js b/src/api/fetchCodeRepos.js index a0f87ea..5f6dba4 100644 --- a/src/api/fetchCodeRepos.js +++ b/src/api/fetchCodeRepos.js @@ -1,23 +1,7 @@ import { handleError } from '../ui/render.js'; -import { normalizeTag, filterDisplayTags } from '../utils/normalizeTag.js'; +import { getPlatformVals } from '../utils/definePlatformVals.js'; import { getPlatformDisplay } from '../utils/defineRibbonVals.js'; - -// Define platform-specific values -const PLATFORM_CONFIGS = { - github: { - starsKey: 'stargazers_count', - profileRepo: '.github' - }, - /* gitlab: { - starsKey: 'star_count', - profileRepo: 'gitlab-profile', - //TODO - }, */ - codeberg: { - starsKey: 'stars_count', - profileRepo: '.profile' - } -}; +import { normalizeTag, filterDisplayTags } from '../utils/normalizeTag.js'; /** * Function for fetching code repositories from the specified platform (GitHub, GitLab, or Codeberg). @@ -46,8 +30,7 @@ export async function fetchCodeRepos( let allRepos = []; let nextUrl = `${orgApiUrl}`; // get platform-specific keys - const platformConfig = PLATFORM_CONFIGS[platform.toLowerCase()]; - const { starsKey, profileRepo } = platformConfig; + const { starsKey, profileRepo, fullNameKey, forkKey, urlKey } = getPlatformVals(platform); try { while (nextUrl) { const ghResponse = await fetch(nextUrl); @@ -68,7 +51,7 @@ export async function fetchCodeRepos( // For org-owned entries in additionalRepos, reuse data already in allRepos to avoid redundant API calls. // Only fetch entries that belong to a different org (external repos). - const allReposByFullName = new Map(allRepos.map(r => [r.full_name, r])); + const allReposByFullName = new Map(allRepos.map(r => [r[fullNameKey], r])); const toFetch = additionalRepos.filter(ownerRepo => !allReposByFullName.has(ownerRepo)); const fromAllRepos = additionalRepos.map(ownerRepo => allReposByFullName.get(ownerRepo)).filter(Boolean); @@ -91,11 +74,11 @@ export async function fetchCodeRepos( const filteredAdditionalRepos = [...fromAllRepos, ...fetchedExternalData.filter(Boolean)]; // Keep only non-forks from org; deduplicate against additional repos by full_name - const orgRepoNames = new Set(filteredAdditionalRepos.map(r => r.full_name)); + const orgRepoNames = new Set(filteredAdditionalRepos.map(r => r[fullNameKey])); const orgNonForks = allRepos.filter(repo => repo.name !== profileRepo && - !repo.fork && - !orgRepoNames.has(repo.full_name)); + !repo[forkKey] && + !orgRepoNames.has(repo[fullNameKey])); // Process additional repos and all remaining org non-forks to include metadata and 'new' flag as appropriate let processedItems = [...filteredAdditionalRepos, ...orgNonForks] @@ -108,10 +91,10 @@ export async function fetchCodeRepos( const tags = [...new Set(rawTags.flatMap(t => normalizeTag(t)).filter(Boolean))]; const displayTags = filterDisplayTags(rawTags); - const release = releasesMap[repo.full_name] ?? null; + const release = releasesMap[repo[fullNameKey]] ?? null; return { - id: repo.full_name, // "Imageomics/", used as backup if can't get repo.name + id: repo[fullNameKey], // "Imageomics/", used as backup if can't get repo.name repoType: "code", createdAt, lastModified, @@ -120,14 +103,13 @@ export async function fetchCodeRepos( tags, rawTags, displayTags, - description: repo.description || "No description provided.", - html_url: repo.html_url, + description: repo.description, // fallback in display (render.js) + html_url: repo[urlKey], hasNewRelease: release?.isNew ?? false, latestReleaseUrl: release?.url ?? null, latestReleaseTag: release?.tag ?? null, cardData: { pretty_name: repo.name, // , the one used for card title display - description: repo.description, stars: repo[starsKey] ?? 0 } }; diff --git a/src/api/fetchStats.js b/src/api/fetchStats.js index 76f6888..a287db8 100644 --- a/src/api/fetchStats.js +++ b/src/api/fetchStats.js @@ -4,9 +4,10 @@ * @param {string} repoApiUrl - The base API URL for the code platform (e.g., GitHub API URL) * @param {string} organizationName - The organization name for the catalog repository * @param {string} catalogRepoName - The repository name for the catalog itself + * @param {string} releaseSuffix - The suffix used for fetching the latest release information (e.g., 'releases/latest') * @returns {Promise} - A promise that resolves when the stats have been fetched and displayed */ -export const fetchCatalogStats = async (repoApiUrl, organizationName, catalogRepoName) => { +export const fetchCatalogStats = async (repoApiUrl, organizationName, catalogRepoName, releaseSuffix) => { // Helper: Updates text, shows the specific stat, and ensures the divider is visible const update = (textId, containerId, value) => { const el = document.getElementById(textId); @@ -21,7 +22,8 @@ export const fetchCatalogStats = async (repoApiUrl, organizationName, catalogRep }; try { - //TODO: Update stars and forks to support other platforms (GitLab, Codeberg) once implemented + //TODO: Update stars and forks to support other platforms: Add another || star for GitLab, platform isn't passed + // forks_count is shared // 1. Get Stars & Forks const repo = await fetch(`${repoApiUrl}${organizationName}/${catalogRepoName}`).then(r => r.ok ? r.json() : {}); if (repo.stargazers_count !== undefined || repo.stars_count !== undefined) update('gh-stars', 'gh-star-container', repo.stargazers_count || repo.stars_count); @@ -29,7 +31,7 @@ export const fetchCatalogStats = async (repoApiUrl, organizationName, catalogRep // 2. Get Version (Tag) // TODO: Import from package.json - const release = await fetch(`${repoApiUrl}${organizationName}/${catalogRepoName}/releases/latest`).then(r => r.ok ? r.json() : {}); + const release = await fetch(`${repoApiUrl}${organizationName}/${catalogRepoName}/${releaseSuffix}`).then(r => r.ok ? r.json() : {}); if (release.tag_name !== undefined) update('gh-tag', 'gh-version-container', release.tag_name); } catch (e) { diff --git a/src/utils/defineApiUrls.js b/src/utils/defineApiUrls.js deleted file mode 100644 index 2d68025..0000000 --- a/src/utils/defineApiUrls.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Defines API URLs based on the selected platform (GitHub or Codeberg, note: GitLab support under development). - * This allows the rest of the codebase to use these constants when making API calls, - * abstracting away platform-specific URL structures. - * - * Usage: import { getPlatformApiUrls } from './defineApiUrls.js'; - * - * Input: platform and organizationName (e.g., 'github' and 'imageomics'), defined from config.yaml and passed to this function. - * Output: platformApiUrls[platform] = { org: ORG_API_URL, repo: REPO_API_URL } - */ - -/** - * Utility function to get the platform-specific API URLs for organization repos and individual repo details. - * @param {string} platform - 'github' or 'codeberg', pending: 'gitlab' - * @param {string} organizationName - The name of the organization (used in URL construction) - * @returns {object} An object containing ORG_API_URL and REPO_API_URL - */ -export function getPlatformApiUrls(platform, organizationName) { - const platformApiUrls = { - github: { - org: `https://api.github.com/orgs/${organizationName}/repos?type=public&per_page=100`, - repo: "https://api.github.com/repos/" - }, - // gitlab: { - // org: `https://gitlab.com/api/v4/groups/${organizationName}/projects?per_page=100`, - // repo: "https://gitlab.com/api/v4/projects/" - // }, - codeberg: { - org: `https://codeberg.org/api/v1/orgs/${organizationName}/repos?limit=50`, - repo: "https://codeberg.org/api/v1/repos/" - } - }; - return platformApiUrls[platform.toLowerCase()]; -} diff --git a/src/utils/definePlatformVals.js b/src/utils/definePlatformVals.js new file mode 100644 index 0000000..2cb9784 --- /dev/null +++ b/src/utils/definePlatformVals.js @@ -0,0 +1,78 @@ +/** + * Collection of functions to define platform-specific values for the catalog, such as API URLs, display values, and repo data keys. + * Each function returns dictionaries with the platform values that have been keyed by platform name (e.g., 'github', 'codeberg', 'gitlab') and potentially other instance-specific values (e.g., organizationName, repoApiUrl). + * Platform and organization name are defined from config.yaml and passed to the functions requiring them. +*/ + + +/** + * Utility function to get platform-specific values for repo data keys (e.g., star count and profile repo name). + * @param {string} platform + * @returns {Object} platformVals - An object containing platform-specific values for repo data keys (e.g., star count and profile repo name) + */ +export function getPlatformVals(platform) { + const platformVals = { + github: { + starsKey: 'stargazers_count', + profileRepo: '.github', + fullNameKey: 'full_name', + forkKey: 'fork', //forks_count is shared + urlKey: 'html_url', + releasePublishedAtKey: 'published_at' + }, + /* gitlab: { + starsKey: 'star_count', + profileRepo: 'gitlab-profile', + fullNameKey: 'name_with_namespace', + forkKey: 'forked_from_project', + urlKey: 'web_url', + releasePublishedAtKey: 'released_at' + }, */ + codeberg: { + starsKey: 'stars_count', + profileRepo: '.profile', + fullNameKey: 'full_name', + forkKey: 'fork', + urlKey: 'html_url', + releasePublishedAtKey: 'published_at' + } +}; + return platformVals[platform.toLowerCase()]; +} + + +/** + * Utility function to get the platform-specific API URLs for organization repos and individual repo details. + * Defines API URLs based on the selected platform (GitHub or Codeberg, note: GitLab support under development). + * This allows the rest of the codebase to use these constants when making API calls, + * abstracting away platform-specific URL structures. + * + * Usage: import { getPlatformApiUrls } from './definePlatformVals.js'; + * + * Input: platform and organizationName (e.g., 'github' and 'imageomics'), defined from config.yaml and passed to this function. + * Output: platformApiUrls[platform] = { org: ORG_API_URL, repo: REPO_API_URL, releaseSuffix: RELEASE_SUFFIX } + * + * @param {string} platform - 'github' or 'codeberg', pending: 'gitlab' + * @param {string} organizationName - The name of the organization (used in URL construction) + * @returns {object} An object containing ORG_API_URL, REPO_API_URL, and RELEASE_SUFFIX + */ +export function getPlatformApiUrls(platform, organizationName) { + const platformApiUrls = { + github: { + org: `https://api.github.com/orgs/${organizationName}/repos?type=public&per_page=100`, + repo: "https://api.github.com/repos/", + releaseSuffix: "releases/latest" + }, + // gitlab: { + // org: `https://gitlab.com/api/v4/groups/${organizationName}/projects?per_page=100`, + // repo: "https://gitlab.com/api/v4/projects/", + // releaseSuffix: "releases/permalink/latest" + // }, + codeberg: { + org: `https://codeberg.org/api/v1/orgs/${organizationName}/repos?limit=50`, + repo: "https://codeberg.org/api/v1/repos/", + releaseSuffix: "releases/latest" + } + }; + return platformApiUrls[platform.toLowerCase()]; +} diff --git a/tests/api/fetchCodeRepos.test.js b/tests/api/fetchCodeRepos.test.js index 55e815e..6ae0fdd 100644 --- a/tests/api/fetchCodeRepos.test.js +++ b/tests/api/fetchCodeRepos.test.js @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { fetchCodeRepos } from '../../src/api/fetchCodeRepos.js'; import { handleError } from '../../src/ui/render.js'; +import { getPlatformVals, getPlatformApiUrls } from '../../src/utils/definePlatformVals.js'; // Mock the internal dependencies vi.mock('../../src/utils/normalizeTag.js', () => ({ @@ -10,34 +11,34 @@ vi.mock('../../src/utils/normalizeTag.js', () => ({ vi.mock('../../src/ui/render.js', () => ({ handleError: vi.fn() })); -vi.mock('../../src/utils/defineRibbonVals.js', () => ({ - getPlatformDisplay: vi.fn(() => 'GitHub') -})); -// define configs for each platform -const platformConfigs = [ - { - name: 'github', - orgApiUrl: 'https://api.github.com/orgs/test-org/repos', - repoApiUrl: 'https://api.github.com/repos/', - platformProfileRepo: '.github', - starsKey: 'stargazers_count' - }, - /* { - name: 'gitlab', - orgApiUrl: 'https://gitlab.com/api/v4/groups/test-org/projects', - repoApiUrl: 'https://gitlab.com/api/v4/projects/', - platformProfileRepo: 'gitlab-profile', - starsKey: 'star_count' - }, */ - { - name: 'codeberg', - orgApiUrl: 'https://codeberg.org/api/v1/orgs/test-org/repos', - repoApiUrl: 'https://codeberg.org/api/v1/repos/', - platformProfileRepo: '.profile', - starsKey: 'stars_count' +const SUPPORTED_PLATFORMS = ['github', 'codeberg']; // 'gitlab' is pending, tests should work on implementation +const TEST_ORG = 'test-org'; + +const platformConfigs = SUPPORTED_PLATFORMS.map(platform => { + const platformVals = getPlatformVals(platform); + const urls = getPlatformApiUrls(platform, TEST_ORG); + + return { + name: platform, + orgApiUrl: urls.org, + repoApiUrl: urls.repo, + starsKey: platformVals.starsKey, + platformProfileRepo: platformVals.profileRepo, + forkKey: platformVals.forkKey, + fullNameKey: platformVals.fullNameKey, + urlKey: platformVals.urlKey, + }; +}); + +const getMockForkValue = (isFork, platform) => { + if (!isFork) { + // GitLab leaves non-forks as undefined; GitHub/Codeberg return false + return platform === 'gitlab' ? undefined : false; } -]; + // GitLab returns parent project object; GitHub/Codeberg return true + return platform === 'gitlab' ? { id: 999 } : true; +}; describe.each(platformConfigs)('fetchCodeRepos - $name', (platformConfig) => { const originalFetch = global.fetch; @@ -55,6 +56,9 @@ describe.each(platformConfigs)('fetchCodeRepos - $name', (platformConfig) => { const platform = platformConfig.name; const starsKey = platformConfig.starsKey; const platformProfileRepo = platformConfig.platformProfileRepo; + const forkKey = platformConfig.forkKey; + const fullNameKey = platformConfig.fullNameKey; + const urlKey = platformConfig.urlKey; const refreshIntervalDays = 30; const releasesMap = {}; @@ -64,18 +68,21 @@ describe.each(platformConfigs)('fetchCodeRepos - $name', (platformConfig) => { const oldDateISO = new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(); // 45 days ago const oldestDateISO = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(); // 90 days ago - it('maps raw repo data and attaches release data from releasesMap', async () => { + it('maps raw repo data, resolves platform keys, and attaches release data', async () => { // Mock a single page platform response global.fetch.mockResolvedValueOnce({ ok: true, headers: { get: () => null }, // No 'link' header means no pagination json: () => Promise.resolve([{ - full_name: 'test-org/code-repo', + [fullNameKey]: 'test-org/code-repo', name: 'code-repo', + description: 'A test repository', + [forkKey]: getMockForkValue(false, platform), updated_at: now.toISOString(), created_at: recentDateISO, topics: ['python'], [starsKey]: 42, + [urlKey]: 'http://example.com/test-org/code-repo' }]) }); @@ -94,15 +101,23 @@ describe.each(platformConfigs)('fetchCodeRepos - $name', (platformConfig) => { ); expect(items).toHaveLength(1); + expect(items[0].id).toBe('test-org/code-repo'); expect(items[0].repoType).toBe('code'); + expect(items[0].description).toBe('A test repository'); + // fork is used for inclusion filtering only and is not returned by fetchCodeRepos expect(items[0].hasNewRelease).toBe(true); expect(items[0].latestReleaseTag).toBe('v1.0'); expect(items[0].tags).toContain('python'); + expect(items[0].html_url).toBe('http://example.com/test-org/code-repo'); + expect(items[0].cardData.pretty_name).toBe('code-repo'); expect(items[0].cardData.stars).toBe(42); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith(orgApiUrl); }); - // Pagination test: simulate multiple pages of GitHub API results using the Link header + // Pagination test: simulate multiple pages of API results using the Link header it('handles multi-page pagination', async () => { global.fetch.mockImplementation((url) => { if (url === orgApiUrl) { @@ -115,7 +130,7 @@ describe.each(platformConfigs)('fetchCodeRepos - $name', (platformConfig) => { : null }, json: () => Promise.resolve([{ - full_name: 'test-org/repo-page1', + [fullNameKey]: 'test-org/repo-page1', name: 'repo-page1', created_at: recentDateISO, updated_at: recentDateISO @@ -127,7 +142,7 @@ describe.each(platformConfigs)('fetchCodeRepos - $name', (platformConfig) => { ok: true, headers: { get: () => null }, json: () => Promise.resolve([{ - full_name: 'test-org/repo-page2', + [fullNameKey]: 'test-org/repo-page2', name: 'repo-page2', created_at: recentDateISO, updated_at: recentDateISO }]) @@ -156,14 +171,14 @@ describe.each(platformConfigs)('fetchCodeRepos - $name', (platformConfig) => { it('fetches additional org repos once and fetches external repos', async () => { // Base organization discovery lists 'test-org/internal-additional' const mockOrgRepo = { - full_name: 'test-org/internal-additional', + [fullNameKey]: 'test-org/internal-additional', name: 'internal-additional', created_at: recentDateISO, updated_at: recentDateISO }; const mockExternalRepo = { - full_name: 'external-org/external-additional', + [fullNameKey]: 'external-org/external-additional', name: 'external-additional', created_at: recentDateISO, updated_at: recentDateISO @@ -224,30 +239,30 @@ describe.each(platformConfigs)('fetchCodeRepos - $name', (platformConfig) => { headers: { get: () => null }, json: () => Promise.resolve([ { - full_name: 'test-org/valid-repo', + [fullNameKey]: 'test-org/valid-repo', name: 'valid-repo', - fork: false, + [forkKey]: getMockForkValue(false, platform), created_at: recentDateISO, updated_at: recentDateISO }, { - full_name: 'test-org/forked-repo', + [fullNameKey]: 'test-org/forked-repo', name: 'forked-repo', - fork: true, + [forkKey]: getMockForkValue(true, platform), created_at: recentDateISO, updated_at: recentDateISO }, { - full_name: 'test-org/forked-in-list', + [fullNameKey]: 'test-org/forked-in-list', name: 'forked-in-list', - fork: true, + [forkKey]: getMockForkValue(true, platform), created_at: recentDateISO, updated_at: recentDateISO }, { - full_name: `test-org/${platformProfileRepo}`, + [fullNameKey]: `test-org/${platformProfileRepo}`, name: platformProfileRepo, - fork: false, + [forkKey]: getMockForkValue(false, platform), created_at: recentDateISO, updated_at: recentDateISO } @@ -280,13 +295,13 @@ describe.each(platformConfigs)('fetchCodeRepos - $name', (platformConfig) => { headers: { get: () => null }, json: () => Promise.resolve([ { - full_name: 'test-org/new-repo', + [fullNameKey]: 'test-org/new-repo', name: 'new-repo', created_at: recentDateISO, updated_at: now.toISOString() }, { - full_name: 'test-org/old-repo', + [fullNameKey]: 'test-org/old-repo', name: 'old-repo', created_at: oldestDateISO, updated_at: oldDateISO