diff --git a/.talismanrc b/.talismanrc index 2d965db3f..74fb868ca 100644 --- a/.talismanrc +++ b/.talismanrc @@ -31,4 +31,6 @@ fileignoreconfig: checksum: 163c7e519561f2c1f473f6f0c5303a2a47c9f5c231b638cd90782c37eaa4859e - filename: packages/contentstack-branches/README.md checksum: 2978e9a9c151cbbafb5dd542edf6815ccec12172ae4ca114a6c4e5e73a85a2b5 + - filename: packages/contentstack-seed/README.md + checksum: ee6667dc5ce3a11517663ed19c090cac9ddfadf562a117b1f165220eb61cdc2b version: '1.0' diff --git a/packages/contentstack-seed/README.md b/packages/contentstack-seed/README.md index bafa0e2cf..75faf55f9 100644 --- a/packages/contentstack-seed/README.md +++ b/packages/contentstack-seed/README.md @@ -1,11 +1,11 @@ ## Description The “seed” command in Contentstack CLI allows users to import content to your stack, from Github repositories. It's an effective command that can help you to migrate content to your stack with minimal steps. -To import content to your stack, you can choose from the following two sources: +To import content to your stack, you can use either of the following: -**Contentstack’s organization**: In this organization, we have provided sample content, which you can import directly to your stack using the seed command. +**Curated official stacks**: When you run the seed command without a full `owner/repo` on `--repo`, the CLI offers a fixed list of official Contentstack seed repositories on GitHub (no GitHub search API). -**Github’s repository**: You can import content available on Github’s repository belonging to an organization or an individual. +**Any GitHub repository**: You can also import content from another GitHub repository by passing `--repo` in `owner/repository` form (organization, user, or enterprise account). ## Commands diff --git a/packages/contentstack-seed/src/seed/github/client.ts b/packages/contentstack-seed/src/seed/github/client.ts index 066913fb7..6ce3d4717 100644 --- a/packages/contentstack-seed/src/seed/github/client.ts +++ b/packages/contentstack-seed/src/seed/github/client.ts @@ -9,7 +9,6 @@ import GithubError from './error'; export default class GitHubClient { readonly gitHubRepoUrl: string; - readonly gitHubUserUrl: string; private readonly httpClient: HttpClient; static parsePath(path?: string) { @@ -30,21 +29,11 @@ export default class GitHubClient { return result; } - constructor(public username: string, defaultStackPattern: string) { + constructor(public username: string) { this.gitHubRepoUrl = `https://api.github.com/repos/${username}`; - this.gitHubUserUrl = `https://api.github.com/search/repositories?q=org%3A${username}+in:name+${defaultStackPattern}`; this.httpClient = HttpClient.create(); } - async getAllRepos(count = 100) { - try { - const response = await this.httpClient.get(`${this.gitHubUserUrl}&per_page=${count}`); - return response.data.items; - } catch (error) { - throw this.buildError(error); - } - } - async getLatest(repo: string, destination: string): Promise { const tarballUrl = await this.getLatestTarballUrl(repo); const releaseStream = await this.streamRelease(tarballUrl); diff --git a/packages/contentstack-seed/src/seed/index.ts b/packages/contentstack-seed/src/seed/index.ts index 31e6fb9aa..9ae937e20 100644 --- a/packages/contentstack-seed/src/seed/index.ts +++ b/packages/contentstack-seed/src/seed/index.ts @@ -4,17 +4,17 @@ import { cliux } from '@contentstack/cli-utilities'; import * as importer from '../seed/importer'; import ContentstackClient, { Organization, Stack } from '../seed/contentstack/client'; import { + inquireOfficialSeedStack, inquireOrganization, inquireProceed, - inquireRepo, inquireStack, InquireStackResponse, } from '../seed/interactive'; import GitHubClient from './github/client'; import GithubError from './github/error'; +import { OFFICIAL_SEED_STACKS } from './seed-stacks'; const DEFAULT_OWNER = 'contentstack'; -const DEFAULT_STACK_PATTERN = 'stack-'; export const ENGLISH_LOCALE = 'en-us'; @@ -38,7 +38,7 @@ export default class ContentModelSeeder { private readonly parent: any = null; private readonly csClient: ContentstackClient; - private readonly ghClient: GitHubClient; + private ghClient: GitHubClient; private readonly _options: ContentModelSeederOptions; @@ -61,7 +61,7 @@ export default class ContentModelSeeder { this.managementToken = options.managementToken; this.csClient = new ContentstackClient(options.cmaHost, limit); - this.ghClient = new GitHubClient(this.ghUsername, DEFAULT_STACK_PATTERN); + this.ghClient = new GitHubClient(this.ghUsername); } async run() { @@ -233,15 +233,9 @@ export default class ContentModelSeeder { } async inquireGitHubRepo() { - try { - const allRepos = await this.ghClient.getAllRepos(); - const stackRepos = allRepos.filter((repo: any) => repo.name.startsWith(DEFAULT_STACK_PATTERN)); - const repoResponse = await inquireRepo(stackRepos); - this.ghRepo = repoResponse.choice; - } catch (error) { - cliux.error( - `Unable to find any Stack repositories within the '${this.ghUsername}' GitHub account. Please re-run this command with a GitHub repository in the 'account/repo' format. You can also re-run the command without arguments to pull from the official Stack list.`, - ); - } + const selected = await inquireOfficialSeedStack(OFFICIAL_SEED_STACKS); + this.ghUsername = selected.owner; + this.ghRepo = selected.repo; + this.ghClient = new GitHubClient(this.ghUsername); } } \ No newline at end of file diff --git a/packages/contentstack-seed/src/seed/interactive.ts b/packages/contentstack-seed/src/seed/interactive.ts index af10584ac..35e9733fc 100644 --- a/packages/contentstack-seed/src/seed/interactive.ts +++ b/packages/contentstack-seed/src/seed/interactive.ts @@ -1,6 +1,11 @@ import inquirer from 'inquirer'; +import { cliux } from '@contentstack/cli-utilities'; + +import { OfficialSeedStack } from './seed-stacks'; import { Organization, Stack } from './contentstack/client'; +export const OFFICIAL_SEED_STACK_SELECTION_MESSAGE = 'Select a stack to import'; + export interface InquireStackResponse { isNew: boolean; name: string | null; @@ -8,6 +13,36 @@ export interface InquireStackResponse { api_key: string | null; } +export async function inquireOfficialSeedStack( + stacks: OfficialSeedStack[], +): Promise<{ owner: string; repo: string }> { + if (!stacks || stacks.length === 0) { + throw new Error('Precondition failed: No official seed stacks configured.'); + } + + const choices = stacks.map((s) => ({ + name: s.displayName, + value: s, + })); + + const response = await inquirer.prompt([ + { + type: 'list', + name: 'stack', + message: OFFICIAL_SEED_STACK_SELECTION_MESSAGE, + choices: [...choices, 'Exit'], + }, + ]); + + if (response.stack === 'Exit') { + cliux.print('Exiting...'); + throw new Error('Exit'); + } + + const selected = response.stack as OfficialSeedStack; + return { owner: selected.owner, repo: selected.repo }; +} + export async function inquireRepo(repos: any[]): Promise<{ choice: string }> { if (!repos || repos.length === 0) throw new Error('Precondition failed: No Repositories found.'); diff --git a/packages/contentstack-seed/src/seed/seed-stacks.ts b/packages/contentstack-seed/src/seed/seed-stacks.ts new file mode 100644 index 000000000..399bccff2 --- /dev/null +++ b/packages/contentstack-seed/src/seed/seed-stacks.ts @@ -0,0 +1,30 @@ +export const OFFICIAL_SEED_OWNER = 'contentstack'; + +export interface OfficialSeedStack { + displayName: string; + owner: string; + repo: string; +} + +export const OFFICIAL_SEED_STACKS: OfficialSeedStack[] = [ + { + displayName: 'Kickstart stack seed', + owner: OFFICIAL_SEED_OWNER, + repo: 'kickstart-stack-seed', + }, + { + displayName: 'Kickstart Veda', + owner: OFFICIAL_SEED_OWNER, + repo: 'kickstart-veda-seed', + }, + { + displayName: 'Compass starter stack', + owner: OFFICIAL_SEED_OWNER, + repo: 'compass-starter-stack', + }, + { + displayName: 'Starter app', + owner: OFFICIAL_SEED_OWNER, + repo: 'stack-starter-app', + }, +]; diff --git a/packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts b/packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts index d5a3a9811..c3102246f 100644 --- a/packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts +++ b/packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts @@ -4,18 +4,28 @@ import { isAuthenticated, configHandler, cliux } from '@contentstack/cli-utiliti // Mock dependencies jest.mock('../../../../src/seed/index'); -jest.mock('@contentstack/cli-utilities', () => ({ - ...jest.requireActual('@contentstack/cli-utilities'), - isAuthenticated: jest.fn(), - configHandler: { - get: jest.fn(), - }, - cliux: { - print: jest.fn(), - loader: jest.fn(), - error: jest.fn(), - }, -})); +jest.mock('@contentstack/cli-utilities', () => { + const { Flags, Command } = require('@oclif/core'); + return { + flags: Flags, + Command, + CLIError: class CLIError extends Error { + constructor(message: string) { + super(message); + this.name = 'CLIError'; + } + }, + isAuthenticated: jest.fn(), + configHandler: { + get: jest.fn(), + }, + cliux: { + print: jest.fn(), + loader: jest.fn(), + error: jest.fn(), + }, + }; +}); describe('SeedCommand', () => { let mockSeeder: jest.Mocked; diff --git a/packages/contentstack-seed/test/seed/contentstack/client.test.ts b/packages/contentstack-seed/test/seed/contentstack/client.test.ts index 880db27e6..dfe46a04e 100644 --- a/packages/contentstack-seed/test/seed/contentstack/client.test.ts +++ b/packages/contentstack-seed/test/seed/contentstack/client.test.ts @@ -1,12 +1,13 @@ // Mock utilities before importing anything that uses them jest.mock('@contentstack/cli-utilities', () => { - const actual = jest.requireActual('@contentstack/cli-utilities'); + const { Flags } = require('@oclif/core'); return { - ...actual, + managementSDKClient: jest.fn(), configHandler: { get: jest.fn().mockReturnValue(null), }, - managementSDKClient: jest.fn(), + ContentstackClient: jest.fn(), + flags: Flags, }; }); diff --git a/packages/contentstack-seed/test/seed/github/client.test.ts b/packages/contentstack-seed/test/seed/github/client.test.ts index 59cca5f18..05c8bd910 100644 --- a/packages/contentstack-seed/test/seed/github/client.test.ts +++ b/packages/contentstack-seed/test/seed/github/client.test.ts @@ -1,16 +1,9 @@ -// Mock utilities before importing anything that uses them -jest.mock('@contentstack/cli-utilities', () => { - const actual = jest.requireActual('@contentstack/cli-utilities'); - return { - ...actual, - configHandler: { - get: jest.fn().mockReturnValue(null), - }, - HttpClient: { - create: jest.fn(), - }, - }; -}); +// Shallow mock: avoid jest.requireActual (pulls uuid ESM in Jest without extra transform config). +jest.mock('@contentstack/cli-utilities', () => ({ + HttpClient: { + create: jest.fn(), + }, +})); // Mock dependencies jest.mock('tar'); @@ -30,7 +23,6 @@ import { Stream } from 'stream'; describe('GitHubClient', () => { let mockHttpClient: any; let githubClient: GitHubClient; - const DEFAULT_STACK_PATTERN = 'stack-'; beforeEach(() => { jest.clearAllMocks(); @@ -44,16 +36,14 @@ describe('GitHubClient', () => { (HttpClient.create as jest.Mock) = jest.fn().mockReturnValue(mockHttpClient); - githubClient = new GitHubClient('testuser', DEFAULT_STACK_PATTERN); + githubClient = new GitHubClient('testuser'); }); describe('constructor', () => { - it('should initialize with username and default stack pattern', () => { - const client = new GitHubClient('testuser', DEFAULT_STACK_PATTERN); + it('should initialize with username', () => { + const client = new GitHubClient('testuser'); expect(client.username).toBe('testuser'); expect(client.gitHubRepoUrl).toBe('https://api.github.com/repos/testuser'); - expect(client.gitHubUserUrl).toContain('testuser'); - expect(client.gitHubUserUrl).toContain(DEFAULT_STACK_PATTERN); }); it('should create HttpClient instance', () => { @@ -88,53 +78,6 @@ describe('GitHubClient', () => { }); - describe('getAllRepos', () => { - it('should fetch all repositories successfully', async () => { - const mockRepos = { - data: { - items: [ - { name: 'stack-repo1', html_url: 'https://github.com/testuser/stack-repo1' }, - { name: 'stack-repo2', html_url: 'https://github.com/testuser/stack-repo2' }, - ], - }, - }; - - mockHttpClient.get.mockResolvedValue(mockRepos); - - const result = await githubClient.getAllRepos(); - - expect(mockHttpClient.get).toHaveBeenCalledWith( - expect.stringContaining('testuser'), - ); - expect(result).toEqual(mockRepos.data.items); - }); - - it('should handle custom count parameter', async () => { - const mockRepos = { data: { items: [] } }; - mockHttpClient.get.mockResolvedValue(mockRepos); - - await githubClient.getAllRepos(50); - - expect(mockHttpClient.get).toHaveBeenCalledWith( - expect.stringContaining('per_page=50'), - ); - }); - - it('should throw GithubError on API failure', async () => { - const mockError = { - response: { - status: 404, - statusText: 'Not Found', - data: { error_message: 'Repository not found' }, - }, - }; - - mockHttpClient.get.mockRejectedValue(mockError); - - await expect(githubClient.getAllRepos()).rejects.toThrow(GithubError); - }); - }); - describe('getLatestTarballUrl', () => { it('should fetch latest release tarball URL', async () => { const mockRelease = { diff --git a/packages/contentstack-seed/test/seed/importer.test.ts b/packages/contentstack-seed/test/seed/importer.test.ts index 1527cb8d3..def5db0de 100644 --- a/packages/contentstack-seed/test/seed/importer.test.ts +++ b/packages/contentstack-seed/test/seed/importer.test.ts @@ -1,17 +1,10 @@ -// Mock utilities before importing -jest.mock('@contentstack/cli-utilities', () => { - const actual = jest.requireActual('@contentstack/cli-utilities'); - return { - ...actual, - configHandler: { - get: jest.fn().mockReturnValue(null), - }, - }; -}); - -// Mock dependencies -jest.mock('@contentstack/cli-cm-import'); -jest.mock('@contentstack/cli-utilities'); +jest.mock('@contentstack/cli-utilities', () => ({ + configHandler: { + get: jest.fn().mockReturnValue(null), + }, + pathValidator: jest.fn((p: string) => p), + sanitizePath: jest.fn((p: string) => p), +})); import * as fs from 'fs'; import * as importer from '../../src/seed/importer'; @@ -31,9 +24,9 @@ describe('Importer', () => { beforeEach(() => { jest.clearAllMocks(); - jest.spyOn(cliUtilities, 'pathValidator').mockImplementation((p: any) => p); - jest.spyOn(cliUtilities, 'sanitizePath').mockImplementation((p: any) => p); - (ImportCommand.run as jest.Mock) = jest.fn().mockResolvedValue(undefined); + (ImportCommand.run as jest.Mock).mockResolvedValue(undefined); + (cliUtilities.pathValidator as jest.Mock).mockImplementation((p: any) => p); + (cliUtilities.sanitizePath as jest.Mock).mockImplementation((p: any) => p); // Mock fs.existsSync: stack folder exists (standard repo structure) jest.spyOn(fs, 'existsSync').mockImplementation((checkPath: fs.PathLike) => { const p = typeof checkPath === 'string' ? checkPath : checkPath.toString(); @@ -155,7 +148,7 @@ describe('Importer', () => { it('should handle import command errors', async () => { const mockError = new Error('Import failed'); - (ImportCommand.run as jest.Mock) = jest.fn().mockRejectedValue(mockError); + (ImportCommand.run as jest.Mock).mockRejectedValueOnce(mockError); await expect(importer.run(mockOptions)).rejects.toThrow('Import failed'); }); diff --git a/packages/contentstack-seed/test/seed/index.test.ts b/packages/contentstack-seed/test/seed/index.test.ts new file mode 100644 index 000000000..ce9f56ecc --- /dev/null +++ b/packages/contentstack-seed/test/seed/index.test.ts @@ -0,0 +1,140 @@ +jest.mock('../../src/seed/importer', () => ({ + run: jest.fn().mockResolvedValue(undefined), +})); + +jest.mock('@contentstack/cli-utilities', () => ({ + cliux: { + print: jest.fn(), + loader: jest.fn(), + error: jest.fn(), + }, + HttpClient: { + create: jest.fn(() => ({ + get: jest.fn(), + options: jest.fn().mockReturnThis(), + resetConfig: jest.fn(), + })), + }, + managementSDKClient: jest.fn(), + configHandler: { + get: jest.fn(), + }, + ContentstackClient: jest.fn(), + pathValidator: jest.fn((p: string) => p), + sanitizePath: jest.fn((p: string) => p), +})); + +import ContentModelSeeder from '../../src/seed/index'; +import GitHubClient from '../../src/seed/github/client'; +import * as interactive from '../../src/seed/interactive'; +import { OFFICIAL_SEED_STACKS } from '../../src/seed/seed-stacks'; + +describe('ContentModelSeeder', () => { + const baseOptions = { + parent: null, + cdaHost: 'https://cdn.contentstack.io', + cmaHost: 'https://api.contentstack.io', + gitHubPath: undefined as string | undefined, + orgUid: undefined as string | undefined, + stackUid: 'stack-api-key' as string | undefined, + stackName: undefined as string | undefined, + fetchLimit: undefined as string | undefined, + skipStackConfirmation: true, + isAuthenticated: true, + managementToken: 'management-token', + alias: undefined as string | undefined, + master_locale: 'en-us', + }; + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('does not define getAllRepos on GitHubClient', () => { + expect((GitHubClient.prototype as unknown as { getAllRepos?: unknown }).getAllRepos).toBeUndefined(); + }); + + describe('getInput when gitHubPath is incomplete', () => { + it('calls inquireOfficialSeedStack with the official catalog', async () => { + const inquireSpy = jest + .spyOn(interactive, 'inquireOfficialSeedStack') + .mockResolvedValue({ owner: 'contentstack', repo: 'stack-starter-app' }); + const makeGetSpy = jest.spyOn(GitHubClient.prototype, 'makeGetApiCall').mockResolvedValue({ + statusCode: 200, + data: {}, + } as never); + + const seeder = new ContentModelSeeder({ + ...baseOptions, + gitHubPath: undefined, + stackUid: 'stack-api-key', + managementToken: 'management-token', + }); + + await seeder.getInput(); + + expect(inquireSpy).toHaveBeenCalledTimes(1); + expect(inquireSpy).toHaveBeenCalledWith(OFFICIAL_SEED_STACKS); + expect(makeGetSpy).toHaveBeenCalledWith('stack-starter-app'); + }); + + it('rebuilds GitHub client for selected owner after official stack selection', async () => { + jest.spyOn(interactive, 'inquireOfficialSeedStack').mockResolvedValue({ + owner: 'contentstack', + repo: 'kickstart-stack-seed', + }); + jest.spyOn(GitHubClient.prototype, 'makeGetApiCall').mockResolvedValue({ + statusCode: 200, + data: {}, + } as never); + + const seeder = new ContentModelSeeder({ + ...baseOptions, + gitHubPath: 'otherorg', + stackUid: 'stack-api-key', + managementToken: 'management-token', + }); + + await seeder.getInput(); + + expect((seeder as unknown as { ghUsername: string }).ghUsername).toBe('contentstack'); + expect((seeder as unknown as { ghRepo: string }).ghRepo).toBe('kickstart-stack-seed'); + }); + + it('does not call makeGetApiCall when user exits from official stack prompt', async () => { + jest.spyOn(interactive, 'inquireOfficialSeedStack').mockRejectedValue(new Error('Exit')); + const makeGetSpy = jest.spyOn(GitHubClient.prototype, 'makeGetApiCall'); + + const seeder = new ContentModelSeeder({ + ...baseOptions, + gitHubPath: undefined, + stackUid: 'stack-api-key', + managementToken: 'management-token', + }); + + await expect(seeder.getInput()).rejects.toThrow('Exit'); + expect(makeGetSpy).not.toHaveBeenCalled(); + }); + }); + + describe('getInput when full gitHubPath is set', () => { + it('does not call inquireOfficialSeedStack', async () => { + const inquireSpy = jest.spyOn(interactive, 'inquireOfficialSeedStack'); + jest.spyOn(GitHubClient.prototype, 'makeGetApiCall').mockResolvedValue({ + statusCode: 200, + data: {}, + } as never); + + const seeder = new ContentModelSeeder({ + ...baseOptions, + gitHubPath: 'acme/custom-seed', + stackUid: 'stack-api-key', + managementToken: 'management-token', + }); + + await seeder.getInput(); + + expect(inquireSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/contentstack-seed/test/seed/interactive.test.ts b/packages/contentstack-seed/test/seed/interactive.test.ts index f3cf43d75..b58d92fc0 100644 --- a/packages/contentstack-seed/test/seed/interactive.test.ts +++ b/packages/contentstack-seed/test/seed/interactive.test.ts @@ -1,6 +1,8 @@ -// Mock inquirer before importing anything +// Mock inquirer before importing anything. +// cli-utilities loads inquirer and calls registerPrompt; the mock must implement it. const mockInquirer = { prompt: jest.fn(), + registerPrompt: jest.fn(), }; jest.mock('inquirer', () => ({ @@ -8,14 +10,95 @@ jest.mock('inquirer', () => ({ default: mockInquirer, })); +jest.mock('@contentstack/cli-utilities', () => ({ + cliux: { + print: jest.fn(), + loader: jest.fn(), + error: jest.fn(), + }, + HttpClient: { + create: jest.fn(() => ({ + get: jest.fn(), + options: jest.fn().mockReturnThis(), + resetConfig: jest.fn(), + })), + }, + managementSDKClient: jest.fn(), + configHandler: { + get: jest.fn(), + }, + ContentstackClient: jest.fn(), +})); + import * as interactive from '../../src/seed/interactive'; import { Organization, Stack } from '../../src/seed/contentstack/client'; +import { cliux } from '@contentstack/cli-utilities'; describe('Interactive', () => { beforeEach(() => { jest.clearAllMocks(); }); + describe('inquireOfficialSeedStack', () => { + const sampleStacks = [ + { displayName: 'Alpha Stack', owner: 'contentstack', repo: 'alpha-repo' }, + { displayName: 'Beta Stack', owner: 'contentstack', repo: 'beta-repo' }, + ]; + + beforeEach(() => { + jest.spyOn(cliux, 'print').mockImplementation(() => undefined); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should throw when catalog is empty', async () => { + await expect(interactive.inquireOfficialSeedStack([])).rejects.toThrow( + 'Precondition failed: No official seed stacks configured.', + ); + }); + + it('should prompt with list and official message', async () => { + mockInquirer.prompt = jest.fn().mockResolvedValue({ + stack: sampleStacks[0], + }); + + const result = await interactive.inquireOfficialSeedStack(sampleStacks); + + expect(mockInquirer.prompt).toHaveBeenCalledWith([ + expect.objectContaining({ + type: 'list', + name: 'stack', + message: interactive.OFFICIAL_SEED_STACK_SELECTION_MESSAGE, + }), + ]); + const promptArg = (mockInquirer.prompt as jest.Mock).mock.calls[0][0][0]; + expect(promptArg.choices).toHaveLength(3); + expect(result).toEqual({ owner: 'contentstack', repo: 'alpha-repo' }); + }); + + it('should include Exit in choices', async () => { + mockInquirer.prompt = jest.fn().mockResolvedValue({ + stack: sampleStacks[1], + }); + + await interactive.inquireOfficialSeedStack(sampleStacks); + + const promptArg = (mockInquirer.prompt as jest.Mock).mock.calls[0][0][0]; + expect(promptArg.choices).toContain('Exit'); + }); + + it('should print and throw on Exit', async () => { + mockInquirer.prompt = jest.fn().mockResolvedValue({ + stack: 'Exit', + }); + + await expect(interactive.inquireOfficialSeedStack(sampleStacks)).rejects.toThrow('Exit'); + expect(cliux.print).toHaveBeenCalledWith('Exiting...'); + }); + }); + describe('inquireRepo', () => { it('should return single repo when only one is provided', async () => { const repos = [ diff --git a/packages/contentstack-seed/test/seed/seed-stacks.test.ts b/packages/contentstack-seed/test/seed/seed-stacks.test.ts new file mode 100644 index 000000000..62e0240ce --- /dev/null +++ b/packages/contentstack-seed/test/seed/seed-stacks.test.ts @@ -0,0 +1,49 @@ +import { + OFFICIAL_SEED_OWNER, + OFFICIAL_SEED_STACKS, +} from '../../src/seed/seed-stacks'; + +describe('seed-stacks', () => { + const expectedRepos = [ + 'kickstart-stack-seed', + 'kickstart-veda-seed', + 'compass-starter-stack', + 'stack-starter-app', + ]; + + it('exports a catalog of four stacks', () => { + expect(Array.isArray(OFFICIAL_SEED_STACKS)).toBe(true); + expect(OFFICIAL_SEED_STACKS).toHaveLength(4); + }); + + it('has exact repo slugs under contentstack', () => { + const repos = OFFICIAL_SEED_STACKS.map((s) => s.repo).sort(); + expect(repos).toEqual([...expectedRepos].sort()); + }); + + it('uses contentstack as owner for every entry', () => { + for (const entry of OFFICIAL_SEED_STACKS) { + expect(entry.owner).toBe(OFFICIAL_SEED_OWNER); + } + }); + + it('has non-empty display names', () => { + for (const entry of OFFICIAL_SEED_STACKS) { + expect(entry.displayName.trim().length).toBeGreaterThan(0); + } + }); + + it('has no duplicate repo slugs', () => { + const repos = OFFICIAL_SEED_STACKS.map((s) => s.repo); + expect(new Set(repos).size).toBe(4); + }); + + it('has stable display names', () => { + expect(OFFICIAL_SEED_STACKS.map((s) => s.displayName)).toEqual([ + 'Kickstart stack seed', + 'Kickstart Veda', + 'Compass starter stack', + 'Starter app', + ]); + }); +});