Skip to content

Commit

Permalink
test github utils (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenkilbourn authored Dec 31, 2024
1 parent 88e93a9 commit ba78bda
Show file tree
Hide file tree
Showing 16 changed files with 853 additions and 4,851 deletions.
4,783 changes: 0 additions & 4,783 deletions package-lock.json

This file was deleted.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"format": "prettier --write .",
"prepare": "husky",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"type-check": "tsc --noEmit"
},
"dependencies": {
Expand Down Expand Up @@ -37,12 +38,13 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@types/lodash": "^4.17.13",
"@types/node": "^20",
"@types/node": "^22.10.2",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "2.1.8",
"eslint": "^9.17.0",
"eslint-config-next": "15.0.3",
"eslint-config-prettier": "^9.1.0",
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"types": ["vitest"],
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
Expand Down
140 changes: 140 additions & 0 deletions utils/CreatePR.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { describe, it, expect, vi, beforeEach, beforeAll, afterAll, Mock } from 'vitest';
import CreatePR from './CreatePR';
import { createOctokit } from './OctokitFactory';
import GetGithubToken from './GetGithubToken';
import { RequestError } from '@octokit/request-error';

vi.mock('./OctokitFactory');
vi.mock('./GetGithubToken');
vi.mock('./FormatFilename', () => ({
formatFilename: (name: string) => name.replace(/\s+/g, '-').toLowerCase(),
}));

describe('CreatePR', () => {
const mockGetRef = vi.fn();
const mockGetTree = vi.fn();
const mockCreateBlob = vi.fn();
const mockCreateTree = vi.fn();
const mockCreateCommit = vi.fn();
const mockCreateRef = vi.fn();
const mockCreatePullRequest = vi.fn();

beforeAll(() => {
vi.stubEnv('TARGET_BRANCH', 'my_branch');
vi.stubEnv('OWNER', 'mockOwner')
vi.stubEnv('REPO', 'mockRepo')
})
afterAll(() => {
vi.unstubAllEnvs();
});

beforeEach(() => {
vi.clearAllMocks();

(GetGithubToken as Mock).mockResolvedValue('mockToken');

(createOctokit as Mock).mockReturnValue({
rest: {
git: {
getRef: mockGetRef,
getTree: mockGetTree,
createBlob: mockCreateBlob,
createTree: mockCreateTree,
createCommit: mockCreateCommit,
createRef: mockCreateRef,
},
pulls: {
create: mockCreatePullRequest,
},
},
});

mockGetRef.mockResolvedValue({ data: { object: { sha: 'mockSha' } } });
mockGetTree.mockResolvedValue({ data: { sha: 'mockTreeSha' } });
mockCreateBlob.mockResolvedValue({ data: { sha: 'mockBlobSha' } });
mockCreateTree.mockResolvedValue({ data: { sha: 'mockNewTreeSha' } });
mockCreateCommit.mockResolvedValue({ data: { sha: 'mockCommitSha' } });
mockCreateRef.mockResolvedValue({});
mockCreatePullRequest.mockResolvedValue({ data: { html_url: 'mockPullRequestUrl' } });
});

it('creates a pull request successfully', async () => {
const mockData = { collection: 'Test Collection', key1: 'value1' };
const result = await CreatePR(mockData);

expect(mockGetRef).toHaveBeenCalledWith({
owner: 'mockOwner',
repo: 'mockRepo',
ref: 'heads/my_branch',
});
expect(result).toBe('mockPullRequestUrl');
});

it('throws an error when RequestError occurs', async () => {
mockGetRef.mockRejectedValue(new Error('Test Error'));
// Suppress console.error for this test
const consoleErrorMock = vi.spyOn(console, 'error').mockImplementation(() => {});

await expect(CreatePR({ collection: 'Test Collection' })).rejects.toThrow('Test Error');

// Restore console.error
consoleErrorMock.mockRestore();
});

it('throws an error when GitHub API returns a 422 error', async () => {
const mockData = { collection: 'Test Collection', key: 'value' };

// Mock 422 error for `octokit.rest.pulls.create`
mockCreatePullRequest.mockRejectedValue(
new RequestError('Validation Failed', 422, {
response: {
data: { message: 'Branch already exists' },
headers: {},
status: 422,
url: 'https://api.github.com/repos/mockOwner/mockRepo/pulls',
},
request: {
method: 'POST',
url: 'https://api.github.com/repos/mockOwner/mockRepo/pulls',
headers: {},
},
})
);

await expect(CreatePR(mockData)).rejects.toThrow('Branch already exists');

expect(GetGithubToken).toHaveBeenCalled();
expect(createOctokit).toHaveBeenCalledWith('mockToken');
expect(mockCreatePullRequest).toHaveBeenCalledWith({
owner: 'mockOwner',
repo: 'mockRepo',
head: 'feat/test-collection',
base: 'my_branch',
title: 'Ingest Request for Test Collection',
});
});

it('creates a pull request successfully in main branch if no TARGET_BRANCH env var supplied', async () => {
delete process.env.TARGET_BRANCH;

const mockData = { collection: 'Test Collection', key1: 'value1' };
const result = await CreatePR(mockData);

expect(mockGetRef).toHaveBeenCalledWith({
owner: 'mockOwner',
repo: 'mockRepo',
ref: 'heads/main',
});
expect(result).toBe('mockPullRequestUrl');

});

it('throws an error when environment variables are missing', async () => {
delete process.env.OWNER;

await expect(CreatePR({ collection: 'Test Collection' })).rejects.toThrow(
'Missing required environment variables: OWNER or REPO'
);
});

});
53 changes: 24 additions & 29 deletions utils/CreatePR.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import { Octokit } from '@octokit/rest';
import { createOctokit } from './OctokitFactory';
import { RequestError } from '@octokit/request-error';
import { formatFilename } from './FormatFilename';
import GetGithubToken from './GetGithubToken';

const targetBranch = process.env.TARGET_BRANCH ||'main';
const owner = process.env.OWNER || '';
const repo = process.env.REPO || '';
interface Data {
collection: string;
[key: string]: unknown;
}

const CreatePR = async (data: Data): Promise<string> => {
const targetBranch = process.env.TARGET_BRANCH || 'main';
const owner = process.env.OWNER;
const repo = process.env.REPO;

if (!owner || !repo) {
throw new Error('Missing required environment variables: OWNER or REPO');
}

const CreatePR = async (data: unknown) => {
try {
//@ts-expect-error testing
const collectionName = data['collection'];
// prettify stringify to preserve json formatting
const collectionName = data.collection;
const content = JSON.stringify(data, null, 2);
const targetPath = 'ingestion-data/staging/dataset-config';
const fileName = formatFilename(collectionName);
const path = `${targetPath}/${fileName}.json`;
const branchName = `feat/${fileName}`;

const token = await GetGithubToken();
const octokit = createOctokit(token);

const octokit = new Octokit({
auth: token,
});

// Get the current target branch reference to get the sha
const sha = await octokit.rest.git.getRef({
owner,
repo,
ref: `heads/${targetBranch}`,
});

// Get the tree associated with master, and the content
// of the template file to open the PR with.
const tree = await octokit.rest.git.getTree({
owner,
repo,
tree_sha: sha.data.object.sha,
tree_sha: sha.data.object?.sha,
});

// Create a new blob with the content in formData
const blob = await octokit.rest.git.createBlob({
owner,
repo,
Expand All @@ -61,13 +61,12 @@ const CreatePR = async (data: unknown) => {
base_tree: tree.data.sha,
});

// Create a commit and a reference using the new tree
const newCommit = await octokit.rest.git.createCommit({
owner,
repo,
message: `Create ${path}`,
tree: newTree.data.sha,
parents: [sha.data.object.sha],
parents: [sha.data.object?.sha],
});

await octokit.rest.git.createRef({
Expand All @@ -77,7 +76,6 @@ const CreatePR = async (data: unknown) => {
sha: newCommit.data.sha,
});

// open PR with new file added to targetBranch
const pr = await octokit.rest.pulls.create({
owner,
repo,
Expand All @@ -86,22 +84,19 @@ const CreatePR = async (data: unknown) => {
title: `Ingest Request for ${collectionName}`,
});

const pr_url = pr.data.html_url;
return pr_url;
return pr.data.html_url;
} catch (error) {
console.log(error);
console.error(error);
if (error instanceof RequestError) {
// branch with branchName already exists
if (error['status'] === 422 && error.response) {
console.error('we have an error');
// @ts-expect-error octokit typing issue
const errorMessage = error.response.data.message as string;
if (error.status === 422 && error.response?.data) {
const errorMessage = (error.response.data as { message?: string }).message || 'Unknown error';
console.error(errorMessage);
throw new Error(errorMessage);
}
}
else throw error;
throw error;
}
};


export default CreatePR;
28 changes: 28 additions & 0 deletions utils/FormatFilename.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest';
import { formatFilename } from './FormatFilename';

describe('formatFilename', () => {
it('removes non-alphanumeric characters except dash and underscore', () => {
expect(formatFilename('valid-file_name-123')).toBe('valid-file_name-123');
expect(formatFilename('invalid@file#name!')).toBe('invalidfilename');
});

it('handles strings with only invalid characters', () => {
expect(formatFilename('@#$%^&*()')).toBe('');
expect(formatFilename('!@#$')).toBe('');
});

it('handles empty strings', () => {
expect(formatFilename('')).toBe('');
});

it('does not modify strings that are already valid', () => {
expect(formatFilename('filename-123_456')).toBe('filename-123_456');
expect(formatFilename('another_valid-filename')).toBe('another_valid-filename');
});

it('preserves case sensitivity', () => {
expect(formatFilename('TestFile-Name')).toBe('TestFile-Name');
expect(formatFilename('Upper_and_Lower123')).toBe('Upper_and_Lower123');
});
});
5 changes: 1 addition & 4 deletions utils/FormatFilename.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
/**
* Remove any non-Alphanumeric characters other than dash or underscore for naming files
*
* @param {input} string
* @returns {string}
*/
export const formatFilename = (input: string) => {
export const formatFilename = (input: string): string => {
return input.replace(/[^0-9a-zA-Z_-]/g, '');
};
Loading

0 comments on commit ba78bda

Please sign in to comment.