decap-cms-backend-github
Version:
GitHub backend for Decap CMS
362 lines (289 loc) • 11.3 kB
JavaScript
import { Cursor, CURSOR_COMPATIBILITY_SYMBOL } from 'decap-cms-lib-util';
import GitHubImplementation from '../implementation';
jest.spyOn(console, 'error').mockImplementation(() => {});
describe('github backend implementation', () => {
const config = {
backend: {
repo: 'owner/repo',
open_authoring: false,
api_root: 'https://api.github.com',
},
};
const createObjectURL = jest.fn();
global.URL = {
createObjectURL,
};
createObjectURL.mockReturnValue('displayURL');
beforeEach(() => {
jest.clearAllMocks();
});
describe('forkExists', () => {
it('should return true when repo is fork and parent matches originRepo', async () => {
const gitHubImplementation = new GitHubImplementation(config);
gitHubImplementation.currentUser = jest.fn().mockResolvedValue({ login: 'login' });
global.fetch = jest.fn().mockResolvedValue({
// matching should be case-insensitive
json: () => ({ fork: true, parent: { full_name: 'OWNER/REPO' } }),
});
await expect(gitHubImplementation.forkExists({ token: 'token' })).resolves.toBe(true);
expect(gitHubImplementation.currentUser).toHaveBeenCalledTimes(1);
expect(gitHubImplementation.currentUser).toHaveBeenCalledWith({ token: 'token' });
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(global.fetch).toHaveBeenCalledWith('https://api.github.com/repos/login/repo', {
method: 'GET',
headers: {
Authorization: 'token token',
},
signal: expect.any(AbortSignal),
});
});
it('should return false when repo is not a fork', async () => {
const gitHubImplementation = new GitHubImplementation(config);
gitHubImplementation.currentUser = jest.fn().mockResolvedValue({ login: 'login' });
global.fetch = jest.fn().mockResolvedValue({
// matching should be case-insensitive
json: () => ({ fork: false }),
});
expect.assertions(1);
await expect(gitHubImplementation.forkExists({ token: 'token' })).resolves.toBe(false);
});
it("should return false when parent doesn't match originRepo", async () => {
const gitHubImplementation = new GitHubImplementation(config);
gitHubImplementation.currentUser = jest.fn().mockResolvedValue({ login: 'login' });
global.fetch = jest.fn().mockResolvedValue({
json: () => ({ fork: true, parent: { full_name: 'owner/other_repo' } }),
});
expect.assertions(1);
await expect(gitHubImplementation.forkExists({ token: 'token' })).resolves.toBe(false);
});
});
describe('persistMedia', () => {
const persistFiles = jest.fn();
const mockAPI = {
persistFiles,
};
persistFiles.mockImplementation((_, files) => {
files.forEach((file, index) => {
file.sha = index;
});
});
it('should persist media file', async () => {
const gitHubImplementation = new GitHubImplementation(config);
gitHubImplementation.api = mockAPI;
const mediaFile = {
fileObj: { size: 100, name: 'image.png' },
path: '/media/image.png',
};
expect.assertions(5);
await expect(gitHubImplementation.persistMedia(mediaFile, {})).resolves.toEqual({
id: 0,
name: 'image.png',
size: 100,
displayURL: 'displayURL',
path: 'media/image.png',
});
expect(persistFiles).toHaveBeenCalledTimes(1);
expect(persistFiles).toHaveBeenCalledWith([], [mediaFile], {});
expect(createObjectURL).toHaveBeenCalledTimes(1);
expect(createObjectURL).toHaveBeenCalledWith(mediaFile.fileObj);
});
it('should log and throw error on "persistFiles" error', async () => {
const gitHubImplementation = new GitHubImplementation(config);
gitHubImplementation.api = mockAPI;
const error = new Error('failed to persist files');
persistFiles.mockRejectedValue(error);
const mediaFile = {
value: 'image.png',
fileObj: { size: 100 },
path: '/media/image.png',
};
expect.assertions(5);
await expect(gitHubImplementation.persistMedia(mediaFile)).rejects.toThrowError(error);
expect(persistFiles).toHaveBeenCalledTimes(1);
expect(createObjectURL).toHaveBeenCalledTimes(0);
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(error);
});
});
describe('unpublishedEntry', () => {
const generateContentKey = jest.fn();
const retrieveUnpublishedEntryData = jest.fn();
const mockAPI = {
generateContentKey,
retrieveUnpublishedEntryData,
};
it('should return unpublished entry data', async () => {
const gitHubImplementation = new GitHubImplementation(config);
gitHubImplementation.api = mockAPI;
gitHubImplementation.loadEntryMediaFiles = jest
.fn()
.mockResolvedValue([{ path: 'image.png', id: 'sha' }]);
generateContentKey.mockReturnValue('contentKey');
const data = {
collection: 'collection',
slug: 'slug',
status: 'draft',
diffs: [],
updatedAt: 'updatedAt',
};
retrieveUnpublishedEntryData.mockResolvedValue(data);
const collection = 'posts';
const slug = 'slug';
await expect(gitHubImplementation.unpublishedEntry({ collection, slug })).resolves.toEqual(
data,
);
expect(generateContentKey).toHaveBeenCalledTimes(1);
expect(generateContentKey).toHaveBeenCalledWith('posts', 'slug');
expect(retrieveUnpublishedEntryData).toHaveBeenCalledTimes(1);
expect(retrieveUnpublishedEntryData).toHaveBeenCalledWith('contentKey');
});
});
describe('entriesByFolder', () => {
const listFiles = jest.fn();
const readFile = jest.fn();
const readFileMetadata = jest.fn(() => Promise.resolve({ author: '', updatedOn: '' }));
const mockAPI = {
listFiles,
readFile,
readFileMetadata,
originRepoURL: 'originRepoURL',
};
it('should return entries and cursor', async () => {
const gitHubImplementation = new GitHubImplementation(config);
gitHubImplementation.api = mockAPI;
const files = [];
const count = 1501;
for (let i = 0; i < count; i++) {
const id = `${i}`.padStart(`${count}`.length, '0');
files.push({
id,
path: `posts/post-${id}.md`,
});
}
listFiles.mockResolvedValue(files);
readFile.mockImplementation((path, id) => Promise.resolve(`${id}`));
const expectedEntries = files
.slice(0, 20)
.map(({ id, path }) => ({ data: id, file: { path, id, author: '', updatedOn: '' } }));
const expectedCursor = Cursor.create({
actions: ['next', 'last'],
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
data: { files },
});
expectedEntries[CURSOR_COMPATIBILITY_SYMBOL] = expectedCursor;
const result = await gitHubImplementation.entriesByFolder('posts', 'md', 1);
expect(result).toEqual(expectedEntries);
expect(listFiles).toHaveBeenCalledTimes(1);
expect(listFiles).toHaveBeenCalledWith('posts', { depth: 1, repoURL: 'originRepoURL' });
expect(readFile).toHaveBeenCalledTimes(20);
});
});
describe('traverseCursor', () => {
const listFiles = jest.fn();
const readFile = jest.fn((path, id) => Promise.resolve(`${id}`));
const readFileMetadata = jest.fn(() => Promise.resolve({}));
const mockAPI = {
listFiles,
readFile,
originRepoURL: 'originRepoURL',
readFileMetadata,
};
const files = [];
const count = 1501;
for (let i = 0; i < count; i++) {
const id = `${i}`.padStart(`${count}`.length, '0');
files.push({
id,
path: `posts/post-${id}.md`,
});
}
it('should handle next action', async () => {
const gitHubImplementation = new GitHubImplementation(config);
gitHubImplementation.api = mockAPI;
const cursor = Cursor.create({
actions: ['next', 'last'],
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
data: { files },
});
const expectedEntries = files
.slice(20, 40)
.map(({ id, path }) => ({ data: id, file: { path, id } }));
const expectedCursor = Cursor.create({
actions: ['prev', 'first', 'next', 'last'],
meta: { page: 2, count, pageSize: 20, pageCount: 76 },
data: { files },
});
const result = await gitHubImplementation.traverseCursor(cursor, 'next');
expect(result).toEqual({
entries: expectedEntries,
cursor: expectedCursor,
});
});
it('should handle prev action', async () => {
const gitHubImplementation = new GitHubImplementation(config);
gitHubImplementation.api = mockAPI;
const cursor = Cursor.create({
actions: ['prev', 'first', 'next', 'last'],
meta: { page: 2, count, pageSize: 20, pageCount: 76 },
data: { files },
});
const expectedEntries = files
.slice(0, 20)
.map(({ id, path }) => ({ data: id, file: { path, id } }));
const expectedCursor = Cursor.create({
actions: ['next', 'last'],
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
data: { files },
});
const result = await gitHubImplementation.traverseCursor(cursor, 'prev');
expect(result).toEqual({
entries: expectedEntries,
cursor: expectedCursor,
});
});
it('should handle last action', async () => {
const gitHubImplementation = new GitHubImplementation(config);
gitHubImplementation.api = mockAPI;
const cursor = Cursor.create({
actions: ['next', 'last'],
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
data: { files },
});
const expectedEntries = files
.slice(1500)
.map(({ id, path }) => ({ data: id, file: { path, id } }));
const expectedCursor = Cursor.create({
actions: ['prev', 'first'],
meta: { page: 76, count, pageSize: 20, pageCount: 76 },
data: { files },
});
const result = await gitHubImplementation.traverseCursor(cursor, 'last');
expect(result).toEqual({
entries: expectedEntries,
cursor: expectedCursor,
});
});
it('should handle first action', async () => {
const gitHubImplementation = new GitHubImplementation(config);
gitHubImplementation.api = mockAPI;
const cursor = Cursor.create({
actions: ['prev', 'first'],
meta: { page: 76, count, pageSize: 20, pageCount: 76 },
data: { files },
});
const expectedEntries = files
.slice(0, 20)
.map(({ id, path }) => ({ data: id, file: { path, id } }));
const expectedCursor = Cursor.create({
actions: ['next', 'last'],
meta: { page: 1, count, pageSize: 20, pageCount: 76 },
data: { files },
});
const result = await gitHubImplementation.traverseCursor(cursor, 'first');
expect(result).toEqual({
entries: expectedEntries,
cursor: expectedCursor,
});
});
});
});