UNPKG

@jungvonmatt/sb-migrate

Version:

CLI tool for managing Storyblok schema and content migrations

503 lines 19.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const migration_1 = require("../../utils/migration"); const api_1 = require("../../utils/api"); const storyblok_1 = require("../../utils/storyblok"); const fs_1 = __importDefault(require("fs")); const setup_1 = require("../setup"); // Mock dependencies vitest_1.vi.mock("../../utils/api", () => ({ api: { stories: { getAll: vitest_1.vi.fn(), get: vitest_1.vi.fn(), update: vitest_1.vi.fn(), }, components: { create: vitest_1.vi.fn(), get: vitest_1.vi.fn(), update: vitest_1.vi.fn(), }, }, })); vitest_1.vi.mock("../../utils/storyblok", () => ({ processMigration: vitest_1.vi.fn(), createRollbackFile: vitest_1.vi.fn(), addOrUpdateDatasource: vitest_1.vi.fn(), addOrUpdateFields: vitest_1.vi.fn((instance, fields) => ({ ...instance, schema: fields, })), moveToTab: vitest_1.vi.fn((instance, name, keys) => ({ ...instance, schema: { ...instance.schema, [name]: keys, }, })), })); vitest_1.vi.mock("picocolors", () => ({ default: { blue: (str) => str, green: (str) => str, red: (str) => str, yellow: (str) => str, cyan: (str) => str, }, })); vitest_1.vi.mock("fs", () => ({ default: { existsSync: vitest_1.vi.fn(), readdirSync: vitest_1.vi.fn(), statSync: vitest_1.vi.fn(), }, existsSync: vitest_1.vi.fn(), readdirSync: vitest_1.vi.fn(), statSync: vitest_1.vi.fn(), })); (0, vitest_1.describe)("Migration Utilities", () => { (0, vitest_1.describe)("isStoryPublishedWithoutChanges", () => { (0, vitest_1.it)("should return true for published story without changes", () => { // Arrange const story = { published: true, unpublished_changes: false, }; // Act const result = (0, migration_1.isStoryPublishedWithoutChanges)(story); // Assert (0, vitest_1.expect)(result).toBe(true); }); (0, vitest_1.it)("should return false for unpublished story", () => { // Arrange const story = { published: false, unpublished_changes: false, }; // Act const result = (0, migration_1.isStoryPublishedWithoutChanges)(story); // Assert (0, vitest_1.expect)(result).toBe(false); }); (0, vitest_1.it)("should return false for story with unpublished changes", () => { // Arrange const story = { published: true, unpublished_changes: true, }; // Act const result = (0, migration_1.isStoryPublishedWithoutChanges)(story); // Assert (0, vitest_1.expect)(result).toBe(false); }); }); (0, vitest_1.describe)("isStoryWithUnpublishedChanges", () => { (0, vitest_1.it)("should return true for published story with changes", () => { // Arrange const story = { published: true, unpublished_changes: true, }; // Act const result = (0, migration_1.isStoryWithUnpublishedChanges)(story); // Assert (0, vitest_1.expect)(result).toBe(true); }); (0, vitest_1.it)("should return false for unpublished story", () => { // Arrange const story = { published: false, unpublished_changes: true, }; // Act const result = (0, migration_1.isStoryWithUnpublishedChanges)(story); // Assert (0, vitest_1.expect)(result).toBe(false); }); (0, vitest_1.it)("should return false for story without changes", () => { // Arrange const story = { published: true, unpublished_changes: false, }; // Act const result = (0, migration_1.isStoryWithUnpublishedChanges)(story); // Assert (0, vitest_1.expect)(result).toBe(false); }); }); (0, vitest_1.describe)("runMigration", () => { const mockStories = [ { id: 1, full_slug: "test-story-1", content: { component: "test-component" }, published: true, unpublished_changes: false, }, { id: 2, full_slug: "test-story-2", content: { component: "test-component" }, published: true, unpublished_changes: false, }, ]; (0, vitest_1.beforeEach)(() => { vitest_1.vi.clearAllMocks(); api_1.api.stories.getAll.mockResolvedValue({ data: { stories: mockStories }, }); api_1.api.stories.get.mockResolvedValue({ data: { story: mockStories[0] }, }); api_1.api.stories.update.mockResolvedValue({}); storyblok_1.processMigration.mockImplementation((content) => { content.component = "updated-component"; }); }); (0, vitest_1.it)("should execute migration successfully", async () => { // Arrange const migrationFn = vitest_1.vi.fn(); // Act const result = await (0, migration_1.runMigration)("test-component", migrationFn); // Assert (0, vitest_1.expect)(result.executed).toBe(true); (0, vitest_1.expect)(api_1.api.stories.getAll).toHaveBeenCalledWith({ contain_component: "test-component", }); (0, vitest_1.expect)(api_1.api.stories.get).toHaveBeenCalled(); (0, vitest_1.expect)(api_1.api.stories.update).toHaveBeenCalled(); (0, vitest_1.expect)(storyblok_1.processMigration).toHaveBeenCalled(); }); (0, vitest_1.it)("should handle no stories found", async () => { // Arrange api_1.api.stories.getAll.mockResolvedValue({ data: { stories: [] }, }); const migrationFn = vitest_1.vi.fn(); // Act const result = await (0, migration_1.runMigration)("test-component", migrationFn); // Assert (0, vitest_1.expect)(result.executed).toBe(false); (0, vitest_1.expect)(result.motive).toBe("NO_STORIES"); }); (0, vitest_1.it)("should handle dry run mode", async () => { // Arrange const migrationFn = vitest_1.vi.fn(); // Act const result = await (0, migration_1.runMigration)("test-component", migrationFn, { isDryrun: true, }); // Assert (0, vitest_1.expect)(result.executed).toBe(true); (0, vitest_1.expect)(api_1.api.stories.update).not.toHaveBeenCalled(); }); (0, vitest_1.it)("should handle errors gracefully", async () => { // Arrange api_1.api.stories.get.mockRejectedValue(new Error("API Error")); const migrationFn = vitest_1.vi.fn(); // Act const result = await (0, migration_1.runMigration)("test-component", migrationFn); // Assert (0, vitest_1.expect)(result.executed).toBe(true); }); }); (0, vitest_1.describe)("showMigrationChanges", () => { (0, vitest_1.beforeEach)(() => { (0, setup_1.clearConsoleOutput)(); }); (0, vitest_1.it)("should show created field", async () => { // Arrange const field = "newField"; const value = "value"; // Act await (0, migration_1.showMigrationChanges)(field, value, undefined); // Assert const output = (0, setup_1.getConsoleOutput)(); (0, vitest_1.expect)(output).toContainEqual({ type: "log", args: [vitest_1.expect.stringContaining('Created field "newField"')], }); }); (0, vitest_1.it)("should show removed field", async () => { // Arrange const field = "oldField"; const oldValue = "value"; // Act await (0, migration_1.showMigrationChanges)(field, undefined, oldValue); // Assert const output = (0, setup_1.getConsoleOutput)(); (0, vitest_1.expect)(output).toContainEqual({ type: "log", args: [vitest_1.expect.stringContaining('Removed the field "oldField"')], }); }); (0, vitest_1.it)("should show updated field", async () => { // Arrange const field = "field"; const newValue = "newValue"; const oldValue = "oldValue"; // Act await (0, migration_1.showMigrationChanges)(field, newValue, oldValue); // Assert const output = (0, setup_1.getConsoleOutput)(); (0, vitest_1.expect)(output).toContainEqual({ type: "log", args: [vitest_1.expect.stringContaining('Updated field "field"')], }); }); }); (0, vitest_1.describe)("defineMigration", () => { (0, vitest_1.it)("should return the same migration object", () => { // Arrange const migration = { type: "create-component", schema: { name: "Test Component", display_name: "Test Component", is_root: false, is_nestable: true, schema: {}, }, }; // Act const result = (0, migration_1.defineMigration)(migration); // Assert (0, vitest_1.expect)(result).toBe(migration); }); (0, vitest_1.it)("should maintain type safety", () => { // Arrange const migration = { type: "create-component", schema: { name: "Test Component", display_name: "Test Component", is_root: false, is_nestable: true, schema: {}, }, }; // Act const result = (0, migration_1.defineMigration)(migration); // Assert (0, vitest_1.expect)(result.type).toBe("create-component"); (0, vitest_1.expect)(result.schema.name).toBe("Test Component"); }); }); (0, vitest_1.describe)("Component", () => { let component; (0, vitest_1.beforeEach)(() => { component = new migration_1.Component(); vitest_1.vi.clearAllMocks(); }); (0, vitest_1.it)("should create a new component", async () => { // Arrange const mockComponent = { name: "test-component", schema: {}, created_at: new Date().toISOString(), id: 1, }; api_1.api.components.create.mockResolvedValue({ data: { component: mockComponent }, }); // Act await component.create("test-component"); // Assert (0, vitest_1.expect)(component.instance).toBe(mockComponent); (0, vitest_1.expect)(api_1.api.components.create).toHaveBeenCalledWith({ name: "test-component", }); }); (0, vitest_1.it)("should load an existing component", async () => { // Arrange const mockComponent = { name: "test-component", schema: {}, created_at: new Date().toISOString(), id: 1, }; api_1.api.components.get.mockResolvedValue({ data: { component: mockComponent }, }); // Act await component.load("test-component"); // Assert (0, vitest_1.expect)(component.instance).toBe(mockComponent); (0, vitest_1.expect)(api_1.api.components.get).toHaveBeenCalledWith("test-component"); }); (0, vitest_1.it)("should add or update fields", () => { // Arrange const mockComponent = { name: "test-component", schema: {}, created_at: new Date().toISOString(), id: 1, }; component.instance = mockComponent; const fields = { field1: { type: "text", pos: 0 }, field2: { type: "number", pos: 1 }, }; // Act component.addOrUpdateFields(fields); // Assert (0, vitest_1.expect)(component.instance.schema).toBeDefined(); }); }); (0, vitest_1.describe)("helper", () => { (0, vitest_1.it)("should update existing component", async () => { // Arrange const mockComponent = { name: "test-component", schema: {}, created_at: new Date().toISOString(), id: 1, }; api_1.api.components.get.mockResolvedValue({ data: { component: mockComponent }, }); // Act const component = await migration_1.helper.updateComponent("test-component"); // Assert (0, vitest_1.expect)(component).toBeInstanceOf(migration_1.Component); (0, vitest_1.expect)(api_1.api.components.get).toHaveBeenCalledWith("test-component"); }); (0, vitest_1.it)("should create new component if not found", async () => { // Arrange api_1.api.components.get.mockRejectedValue(new Error("Not found")); api_1.api.components.create.mockResolvedValue({ data: { component: { name: "test-component", schema: {}, created_at: new Date().toISOString(), id: 1, }, }, }); // Act const component = await migration_1.helper.updateComponent("test-component"); // Assert (0, vitest_1.expect)(component).toBeInstanceOf(migration_1.Component); (0, vitest_1.expect)(api_1.api.components.create).toHaveBeenCalledWith({ name: "test-component", }); }); }); (0, vitest_1.describe)("findMigrations", () => { (0, vitest_1.beforeEach)(() => { vitest_1.vi.clearAllMocks(); }); (0, vitest_1.it)("should return empty array if migrations directory doesn't exist", async () => { // Arrange fs_1.default.existsSync.mockReturnValue(false); // Act const result = await (0, migration_1.findMigrations)(); // Assert (0, vitest_1.expect)(result).toEqual([]); (0, vitest_1.expect)(fs_1.default.existsSync).toHaveBeenCalledWith(vitest_1.expect.stringContaining("migrations")); }); (0, vitest_1.it)("should find .js and .ts files in migrations directory", async () => { // Arrange fs_1.default.existsSync.mockReturnValue(true); fs_1.default.readdirSync.mockReturnValue([ { name: "migration1.js", isFile: () => true, isDirectory: () => false }, { name: "migration2.ts", isFile: () => true, isDirectory: () => false }, { name: "other.txt", isFile: () => true, isDirectory: () => false }, ]); // Act const result = await (0, migration_1.findMigrations)(); // Assert (0, vitest_1.expect)(result).toEqual(["migration1.js", "migration2.ts"]); (0, vitest_1.expect)(result).not.toContain("other.txt"); }); (0, vitest_1.it)("should recursively search subdirectories", async () => { // Arrange fs_1.default.existsSync.mockReturnValue(true); fs_1.default.readdirSync .mockImplementationOnce(() => [ { name: "schema", isFile: () => false, isDirectory: () => true }, { name: "content", isFile: () => false, isDirectory: () => true }, ]) .mockImplementationOnce(() => [ { name: "migration1.js", isFile: () => true, isDirectory: () => false, }, { name: "migration2.ts", isFile: () => true, isDirectory: () => false, }, ]) .mockImplementationOnce(() => [ { name: "migration3.js", isFile: () => true, isDirectory: () => false, }, ]); // Act const result = await (0, migration_1.findMigrations)(); // Assert (0, vitest_1.expect)(result).toEqual([ "content/migration3.js", "schema/migration1.js", "schema/migration2.ts", ]); }); (0, vitest_1.it)("should ignore non-migration files", async () => { // Arrange fs_1.default.existsSync.mockReturnValue(true); fs_1.default.readdirSync.mockReturnValue([ { name: "migration1.js", isFile: () => true, isDirectory: () => false }, { name: "config.json", isFile: () => true, isDirectory: () => false }, { name: "README.md", isFile: () => true, isDirectory: () => false }, { name: "migration2.ts", isFile: () => true, isDirectory: () => false }, ]); // Act const result = await (0, migration_1.findMigrations)(); // Assert (0, vitest_1.expect)(result).toEqual(["migration1.js", "migration2.ts"]); (0, vitest_1.expect)(result).not.toContain("config.json"); (0, vitest_1.expect)(result).not.toContain("README.md"); }); (0, vitest_1.it)("should return sorted list of migrations", async () => { // Arrange fs_1.default.existsSync.mockReturnValue(true); fs_1.default.readdirSync.mockReturnValue([ { name: "z-migration.js", isFile: () => true, isDirectory: () => false, }, { name: "a-migration.ts", isFile: () => true, isDirectory: () => false, }, { name: "m-migration.js", isFile: () => true, isDirectory: () => false, }, ]); // Act const result = await (0, migration_1.findMigrations)(); // Assert (0, vitest_1.expect)(result).toEqual([ "a-migration.ts", "m-migration.js", "z-migration.js", ]); }); }); }); //# sourceMappingURL=migration.test.js.map