mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 17:01:13 +00:00
Prettier 2.0 (#393)
This commit is contained in:
@@ -237,18 +237,18 @@ export class GitHubClient {
|
||||
try {
|
||||
const response = (await this.ocktokit.graphql(repositoryQuery, {
|
||||
owner,
|
||||
repo
|
||||
repo,
|
||||
} as RepositoryQueryParams)) as RepositoryQueryResponse;
|
||||
|
||||
return {
|
||||
status: HttpStatusCodes.OK,
|
||||
data: GitHubClient.toGitHubRepo(response.repository)
|
||||
data: GitHubClient.toGitHubRepo(response.repository),
|
||||
};
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getRepoAsync failed");
|
||||
return {
|
||||
status: GitHubClient.SelfErrorCode,
|
||||
data: undefined
|
||||
data: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -257,19 +257,19 @@ export class GitHubClient {
|
||||
try {
|
||||
const response = (await this.ocktokit.graphql(repositoriesQuery, {
|
||||
pageSize,
|
||||
endCursor
|
||||
endCursor,
|
||||
} as RepositoriesQueryParams)) as RepositoriesQueryResponse;
|
||||
|
||||
return {
|
||||
status: HttpStatusCodes.OK,
|
||||
data: response.viewer.repositories.nodes.map(repo => GitHubClient.toGitHubRepo(repo)),
|
||||
pageInfo: GitHubClient.toGitHubPageInfo(response.viewer.repositories.pageInfo)
|
||||
data: response.viewer.repositories.nodes.map((repo) => GitHubClient.toGitHubRepo(repo)),
|
||||
pageInfo: GitHubClient.toGitHubPageInfo(response.viewer.repositories.pageInfo),
|
||||
};
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getRepoAsync failed");
|
||||
return {
|
||||
status: GitHubClient.SelfErrorCode,
|
||||
data: undefined
|
||||
data: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -286,19 +286,19 @@ export class GitHubClient {
|
||||
repo,
|
||||
refPrefix: "refs/heads/",
|
||||
pageSize,
|
||||
endCursor
|
||||
endCursor,
|
||||
} as BranchesQueryParams)) as BranchesQueryResponse;
|
||||
|
||||
return {
|
||||
status: HttpStatusCodes.OK,
|
||||
data: response.repository.refs.nodes.map(ref => GitHubClient.toGitHubBranch(ref)),
|
||||
pageInfo: GitHubClient.toGitHubPageInfo(response.repository.refs.pageInfo)
|
||||
data: response.repository.refs.nodes.map((ref) => GitHubClient.toGitHubBranch(ref)),
|
||||
pageInfo: GitHubClient.toGitHubPageInfo(response.repository.refs.pageInfo),
|
||||
};
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getBranchesAsync failed");
|
||||
return {
|
||||
status: GitHubClient.SelfErrorCode,
|
||||
data: undefined
|
||||
data: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -315,13 +315,13 @@ export class GitHubClient {
|
||||
repo,
|
||||
ref: `refs/heads/${branch}`,
|
||||
path: path || undefined,
|
||||
objectExpression: `refs/heads/${branch}:${path || ""}`
|
||||
objectExpression: `refs/heads/${branch}:${path || ""}`,
|
||||
} as ContentsQueryParams)) as ContentsQueryResponse;
|
||||
|
||||
if (!response.repository.object) {
|
||||
return {
|
||||
status: HttpStatusCodes.NotFound,
|
||||
data: undefined
|
||||
data: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ export class GitHubClient {
|
||||
const gitHubCommit = GitHubClient.toGitHubCommit(response.repository.ref.target.history.nodes[0]);
|
||||
|
||||
if (Array.isArray(entries)) {
|
||||
data = entries.map(entry =>
|
||||
data = entries.map((entry) =>
|
||||
GitHubClient.toGitHubFile(
|
||||
entry,
|
||||
(path && UrlUtility.createUri(path, entry.name)) || entry.name,
|
||||
@@ -346,7 +346,7 @@ export class GitHubClient {
|
||||
{
|
||||
name: NotebookUtil.getName(path),
|
||||
type: "blob",
|
||||
object: response.repository.object
|
||||
object: response.repository.object,
|
||||
},
|
||||
path,
|
||||
gitHubRepo,
|
||||
@@ -357,13 +357,13 @@ export class GitHubClient {
|
||||
|
||||
return {
|
||||
status: HttpStatusCodes.OK,
|
||||
data
|
||||
data,
|
||||
};
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getContentsAsync failed");
|
||||
return {
|
||||
status: GitHubClient.SelfErrorCode,
|
||||
data: undefined
|
||||
data: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -384,7 +384,7 @@ export class GitHubClient {
|
||||
path,
|
||||
message,
|
||||
content,
|
||||
sha
|
||||
sha,
|
||||
});
|
||||
|
||||
let data: IGitHubCommit;
|
||||
@@ -409,8 +409,8 @@ export class GitHubClient {
|
||||
repo,
|
||||
ref,
|
||||
headers: {
|
||||
"If-None-Match": "" // disable 60s cache
|
||||
}
|
||||
"If-None-Match": "", // disable 60s cache
|
||||
},
|
||||
});
|
||||
|
||||
const currentTree = await this.ocktokit.git.getTree({
|
||||
@@ -419,13 +419,13 @@ export class GitHubClient {
|
||||
tree_sha: currentRef.data.object.sha,
|
||||
recursive: "1",
|
||||
headers: {
|
||||
"If-None-Match": "" // disable 60s cache
|
||||
}
|
||||
"If-None-Match": "", // disable 60s cache
|
||||
},
|
||||
});
|
||||
|
||||
// API infers tree from paths so we need to filter them out
|
||||
const currentTreeItems = currentTree.data.tree.filter(item => item.type !== "tree");
|
||||
currentTreeItems.forEach(item => {
|
||||
const currentTreeItems = currentTree.data.tree.filter((item) => item.type !== "tree");
|
||||
currentTreeItems.forEach((item) => {
|
||||
if (item.path === newPath) {
|
||||
throw new Error("File with the path already exists");
|
||||
}
|
||||
@@ -434,12 +434,12 @@ export class GitHubClient {
|
||||
const updatedTree = await this.ocktokit.git.createTree({
|
||||
owner,
|
||||
repo,
|
||||
tree: currentTreeItems.map(item => ({
|
||||
tree: currentTreeItems.map((item) => ({
|
||||
path: item.path === oldPath ? newPath : item.path,
|
||||
mode: item.mode as "100644" | "100755" | "040000" | "160000" | "120000",
|
||||
type: item.type as "blob" | "tree" | "commit",
|
||||
sha: item.sha
|
||||
}))
|
||||
sha: item.sha,
|
||||
})),
|
||||
});
|
||||
|
||||
const newCommit = await this.ocktokit.git.createCommit({
|
||||
@@ -447,19 +447,19 @@ export class GitHubClient {
|
||||
repo,
|
||||
message,
|
||||
parents: [currentRef.data.object.sha],
|
||||
tree: updatedTree.data.sha
|
||||
tree: updatedTree.data.sha,
|
||||
});
|
||||
|
||||
const updatedRef = await this.ocktokit.git.updateRef({
|
||||
owner,
|
||||
repo,
|
||||
ref,
|
||||
sha: newCommit.data.sha
|
||||
sha: newCommit.data.sha,
|
||||
});
|
||||
|
||||
return {
|
||||
status: updatedRef.status,
|
||||
data: GitHubClient.toGitHubCommit(newCommit.data)
|
||||
data: GitHubClient.toGitHubCommit(newCommit.data),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ export class GitHubClient {
|
||||
path: file.path,
|
||||
message,
|
||||
sha: file.sha,
|
||||
branch: file.branch.name
|
||||
branch: file.branch.name,
|
||||
});
|
||||
|
||||
let data: IGitHubCommit;
|
||||
@@ -487,11 +487,11 @@ export class GitHubClient {
|
||||
repo,
|
||||
file_sha: sha,
|
||||
mediaType: {
|
||||
format: "raw"
|
||||
format: "raw",
|
||||
},
|
||||
headers: {
|
||||
"If-None-Match": "" // disable 60s cache
|
||||
}
|
||||
"If-None-Match": "", // disable 60s cache
|
||||
},
|
||||
});
|
||||
|
||||
return { status: response.status, data: <string>(<unknown>response.data) };
|
||||
@@ -504,11 +504,11 @@ export class GitHubClient {
|
||||
debug: () => {},
|
||||
info: (message?: any) => GitHubClient.log(Logger.logInfo, message),
|
||||
warn: (message?: any) => GitHubClient.log(Logger.logWarning, message),
|
||||
error: (error?: any) => Logger.logError(getErrorMessage(error), "GitHubClient.Octokit")
|
||||
}
|
||||
error: (error?: any) => Logger.logError(getErrorMessage(error), "GitHubClient.Octokit"),
|
||||
},
|
||||
});
|
||||
|
||||
this.ocktokit.hook.error("request", error => {
|
||||
this.ocktokit.hook.error("request", (error) => {
|
||||
this.errorCallback(error);
|
||||
throw error;
|
||||
});
|
||||
@@ -525,13 +525,13 @@ export class GitHubClient {
|
||||
return {
|
||||
owner: object.owner.login,
|
||||
name: object.name,
|
||||
private: object.isPrivate
|
||||
private: object.isPrivate,
|
||||
};
|
||||
}
|
||||
|
||||
private static toGitHubBranch(object: Ref): IGitHubBranch {
|
||||
return {
|
||||
name: object.name
|
||||
name: object.name,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -546,14 +546,14 @@ export class GitHubClient {
|
||||
return {
|
||||
sha: object.sha || object.oid,
|
||||
message: object.message,
|
||||
commitDate: object.committer.date
|
||||
commitDate: object.committer.date,
|
||||
};
|
||||
}
|
||||
|
||||
private static toGitHubPageInfo(object: PageInfo): IGitHubPageInfo {
|
||||
return {
|
||||
endCursor: object.endCursor,
|
||||
hasNextPage: object.hasNextPage
|
||||
hasNextPage: object.hasNextPage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -576,7 +576,7 @@ export class GitHubClient {
|
||||
branch,
|
||||
commit,
|
||||
size: entry.object?.byteSize,
|
||||
sha: entry.object?.oid
|
||||
sha: entry.object?.oid,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
export interface IGitHubConnectorParams {
|
||||
state: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export const GitHubConnectorMsgType = "GitHubConnectorMsgType";
|
||||
|
||||
export class GitHubConnector {
|
||||
public start(params: URLSearchParams, window: Window & typeof globalThis) {
|
||||
window.postMessage(
|
||||
{
|
||||
type: GitHubConnectorMsgType,
|
||||
data: {
|
||||
state: params.get("state"),
|
||||
code: params.get("code")
|
||||
} as IGitHubConnectorParams
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var connector = new GitHubConnector();
|
||||
window.addEventListener("load", () => {
|
||||
const openerWindow = window.opener;
|
||||
if (openerWindow) {
|
||||
connector.start(new URLSearchParams(document.location.search), openerWindow);
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
export interface IGitHubConnectorParams {
|
||||
state: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export const GitHubConnectorMsgType = "GitHubConnectorMsgType";
|
||||
|
||||
export class GitHubConnector {
|
||||
public start(params: URLSearchParams, window: Window & typeof globalThis) {
|
||||
window.postMessage(
|
||||
{
|
||||
type: GitHubConnectorMsgType,
|
||||
data: {
|
||||
state: params.get("state"),
|
||||
code: params.get("code"),
|
||||
} as IGitHubConnectorParams,
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var connector = new GitHubConnector();
|
||||
window.addEventListener("load", () => {
|
||||
const openerWindow = window.opener;
|
||||
if (openerWindow) {
|
||||
connector.start(new URLSearchParams(document.location.search), openerWindow);
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,302 +1,302 @@
|
||||
import { IContent } from "@nteract/core";
|
||||
import { fixture } from "@nteract/fixtures";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { GitHubClient, IGitHubCommit, IGitHubFile } from "./GitHubClient";
|
||||
import { GitHubContentProvider } from "./GitHubContentProvider";
|
||||
import * as GitHubUtils from "../Utils/GitHubUtils";
|
||||
|
||||
const gitHubClient = new GitHubClient(() => {});
|
||||
const gitHubContentProvider = new GitHubContentProvider({
|
||||
gitHubClient,
|
||||
promptForCommitMsg: () => Promise.resolve("commit msg")
|
||||
});
|
||||
const gitHubCommit: IGitHubCommit = {
|
||||
sha: "sha",
|
||||
message: "message",
|
||||
commitDate: "date"
|
||||
};
|
||||
const sampleFile: IGitHubFile = {
|
||||
type: "blob",
|
||||
size: 0,
|
||||
name: "name.ipynb",
|
||||
path: "dir/name.ipynb",
|
||||
content: fixture,
|
||||
sha: "sha",
|
||||
repo: {
|
||||
owner: "owner",
|
||||
name: "repo",
|
||||
private: false
|
||||
},
|
||||
branch: {
|
||||
name: "branch"
|
||||
},
|
||||
commit: gitHubCommit
|
||||
};
|
||||
const sampleGitHubUri = GitHubUtils.toContentUri(
|
||||
sampleFile.repo.owner,
|
||||
sampleFile.repo.name,
|
||||
sampleFile.branch.name,
|
||||
sampleFile.path
|
||||
);
|
||||
const sampleNotebookModel: IContent<"notebook"> = {
|
||||
name: sampleFile.name,
|
||||
path: sampleGitHubUri,
|
||||
type: "notebook",
|
||||
writable: true,
|
||||
created: "",
|
||||
last_modified: "date",
|
||||
mimetype: "application/x-ipynb+json",
|
||||
content: sampleFile.content ? JSON.parse(sampleFile.content) : null,
|
||||
format: "json"
|
||||
};
|
||||
|
||||
describe("GitHubContentProvider remove", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, "invalid path").toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed delete", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.deleteFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("removes notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.NoContent);
|
||||
expect(gitHubClient.deleteFileAsync).toBeCalled();
|
||||
expect(response.response).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider get", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.get(null, "invalid path", null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.get(null, sampleGitHubUri, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("reads notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.get(null, sampleGitHubUri, {}).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(response.response).toEqual(sampleNotebookModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider update", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.update(null, "invalid path", null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed rename", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.renameFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("updates notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.renameFileAsync).toBeCalled();
|
||||
expect(response.response.type).toEqual(sampleNotebookModel.type);
|
||||
expect(response.response.name).toEqual(sampleNotebookModel.name);
|
||||
expect(response.response.path).toEqual(sampleNotebookModel.path);
|
||||
expect(response.response.content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider create", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync");
|
||||
|
||||
const response = await gitHubContentProvider.create(null, "invalid path", sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.createOrUpdateFileAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed create", async () => {
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("creates notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.Created);
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
expect(response.response.type).toEqual(sampleNotebookModel.type);
|
||||
expect(response.response.name).toBeDefined();
|
||||
expect(response.response.path).toBeDefined();
|
||||
expect(response.response.content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider save", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.save(null, "invalid path", null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed update", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("saves notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
expect(response.response.type).toEqual(sampleNotebookModel.type);
|
||||
expect(response.response.name).toEqual(sampleNotebookModel.name);
|
||||
expect(response.response.path).toEqual(sampleNotebookModel.path);
|
||||
expect(response.response.content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider listCheckpoints", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.listCheckpoints(null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider createCheckpoint", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.createCheckpoint(null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider deleteCheckpoint", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.deleteCheckpoint(null, null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider restoreFromCheckpoint", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.restoreFromCheckpoint(null, null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
import { IContent } from "@nteract/core";
|
||||
import { fixture } from "@nteract/fixtures";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { GitHubClient, IGitHubCommit, IGitHubFile } from "./GitHubClient";
|
||||
import { GitHubContentProvider } from "./GitHubContentProvider";
|
||||
import * as GitHubUtils from "../Utils/GitHubUtils";
|
||||
|
||||
const gitHubClient = new GitHubClient(() => {});
|
||||
const gitHubContentProvider = new GitHubContentProvider({
|
||||
gitHubClient,
|
||||
promptForCommitMsg: () => Promise.resolve("commit msg"),
|
||||
});
|
||||
const gitHubCommit: IGitHubCommit = {
|
||||
sha: "sha",
|
||||
message: "message",
|
||||
commitDate: "date",
|
||||
};
|
||||
const sampleFile: IGitHubFile = {
|
||||
type: "blob",
|
||||
size: 0,
|
||||
name: "name.ipynb",
|
||||
path: "dir/name.ipynb",
|
||||
content: fixture,
|
||||
sha: "sha",
|
||||
repo: {
|
||||
owner: "owner",
|
||||
name: "repo",
|
||||
private: false,
|
||||
},
|
||||
branch: {
|
||||
name: "branch",
|
||||
},
|
||||
commit: gitHubCommit,
|
||||
};
|
||||
const sampleGitHubUri = GitHubUtils.toContentUri(
|
||||
sampleFile.repo.owner,
|
||||
sampleFile.repo.name,
|
||||
sampleFile.branch.name,
|
||||
sampleFile.path
|
||||
);
|
||||
const sampleNotebookModel: IContent<"notebook"> = {
|
||||
name: sampleFile.name,
|
||||
path: sampleGitHubUri,
|
||||
type: "notebook",
|
||||
writable: true,
|
||||
created: "",
|
||||
last_modified: "date",
|
||||
mimetype: "application/x-ipynb+json",
|
||||
content: sampleFile.content ? JSON.parse(sampleFile.content) : null,
|
||||
format: "json",
|
||||
};
|
||||
|
||||
describe("GitHubContentProvider remove", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, "invalid path").toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed delete", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.deleteFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("removes notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.NoContent);
|
||||
expect(gitHubClient.deleteFileAsync).toBeCalled();
|
||||
expect(response.response).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider get", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.get(null, "invalid path", null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.get(null, sampleGitHubUri, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("reads notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.get(null, sampleGitHubUri, {}).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(response.response).toEqual(sampleNotebookModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider update", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.update(null, "invalid path", null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed rename", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.renameFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("updates notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.renameFileAsync).toBeCalled();
|
||||
expect(response.response.type).toEqual(sampleNotebookModel.type);
|
||||
expect(response.response.name).toEqual(sampleNotebookModel.name);
|
||||
expect(response.response.path).toEqual(sampleNotebookModel.path);
|
||||
expect(response.response.content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider create", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync");
|
||||
|
||||
const response = await gitHubContentProvider.create(null, "invalid path", sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.createOrUpdateFileAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed create", async () => {
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("creates notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.Created);
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
expect(response.response.type).toEqual(sampleNotebookModel.type);
|
||||
expect(response.response.name).toBeDefined();
|
||||
expect(response.response.path).toBeDefined();
|
||||
expect(response.response.content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider save", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.save(null, "invalid path", null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed update", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("saves notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
expect(response.response.type).toEqual(sampleNotebookModel.type);
|
||||
expect(response.response.name).toEqual(sampleNotebookModel.name);
|
||||
expect(response.response.path).toEqual(sampleNotebookModel.path);
|
||||
expect(response.response.content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider listCheckpoints", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.listCheckpoints(null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider createCheckpoint", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.createCheckpoint(null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider deleteCheckpoint", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.deleteCheckpoint(null, null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider restoreFromCheckpoint", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.restoreFromCheckpoint(null, null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,431 +1,431 @@
|
||||
import { Notebook, stringifyNotebook, makeNotebookRecord, toJS } from "@nteract/commutable";
|
||||
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
||||
import { from, Observable, of } from "rxjs";
|
||||
import { AjaxResponse } from "rxjs/ajax";
|
||||
import * as Base64Utils from "../Utils/Base64Utils";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
||||
import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient";
|
||||
import * as GitHubUtils from "../Utils/GitHubUtils";
|
||||
import UrlUtility from "../Common/UrlUtility";
|
||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||
|
||||
export interface GitHubContentProviderParams {
|
||||
gitHubClient: GitHubClient;
|
||||
promptForCommitMsg: (title: string, primaryButtonLabel: string) => Promise<string>;
|
||||
}
|
||||
|
||||
class GitHubContentProviderError extends Error {
|
||||
constructor(error: string, public errno: number = GitHubContentProvider.SelfErrorCode) {
|
||||
super(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Provides 'contents' API for GitHub
|
||||
// http://jupyter-api.surge.sh/#!/contents
|
||||
export class GitHubContentProvider implements IContentProvider {
|
||||
public static readonly SelfErrorCode = 555;
|
||||
|
||||
constructor(private params: GitHubContentProviderParams) {}
|
||||
|
||||
public remove(_: ServerConfig, uri: string): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Delete", "Delete");
|
||||
const response = await this.params.gitHubClient.deleteFileAsync(content.data as IGitHubFile, commitMsg);
|
||||
if (response.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to delete", response.status);
|
||||
}
|
||||
|
||||
return this.createSuccessAjaxResponse(HttpStatusCodes.NoContent, undefined);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubContentProvider/remove", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public get(_: ServerConfig, uri: string, params: Partial<IGetParams>): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
if (content.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get content", content.status);
|
||||
}
|
||||
|
||||
if (!Array.isArray(content.data) && !content.data.content && params.content !== 0) {
|
||||
const file = content.data;
|
||||
file.content = (
|
||||
await this.params.gitHubClient.getBlobAsync(file.repo.owner, file.repo.name, file.sha)
|
||||
).data;
|
||||
}
|
||||
|
||||
return this.createSuccessAjaxResponse(HttpStatusCodes.OK, this.createContentModel(uri, content.data, params));
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubContentProvider/get", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public update<FT extends FileType>(
|
||||
_: ServerConfig,
|
||||
uri: string,
|
||||
model: Partial<IContent<FT>>
|
||||
): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
const gitHubFile = content.data as IGitHubFile;
|
||||
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Rename", "Rename");
|
||||
const newUri = model.path;
|
||||
const newPath = GitHubUtils.fromContentUri(newUri).path;
|
||||
const response = await this.params.gitHubClient.renameFileAsync(
|
||||
gitHubFile.repo.owner,
|
||||
gitHubFile.repo.name,
|
||||
gitHubFile.branch.name,
|
||||
commitMsg,
|
||||
gitHubFile.path,
|
||||
newPath
|
||||
);
|
||||
if (response.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to rename", response.status);
|
||||
}
|
||||
|
||||
gitHubFile.commit = response.data;
|
||||
gitHubFile.path = newPath;
|
||||
gitHubFile.name = NotebookUtil.getName(gitHubFile.path);
|
||||
|
||||
return this.createSuccessAjaxResponse(
|
||||
HttpStatusCodes.OK,
|
||||
this.createContentModel(newUri, gitHubFile, { content: 0 })
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public create<FT extends FileType>(
|
||||
_: ServerConfig,
|
||||
uri: string,
|
||||
model: Partial<IContent<FT>> & { type: FT }
|
||||
): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.params.promptForCommitMsg("Create New Notebook", "Create").then(async (commitMsg: string) => {
|
||||
try {
|
||||
if (!commitMsg) {
|
||||
throw new GitHubContentProviderError("Couldn't get a commit message");
|
||||
}
|
||||
|
||||
if (model.type !== "notebook") {
|
||||
throw new GitHubContentProviderError("Unsupported content type");
|
||||
}
|
||||
|
||||
const contentInfo = GitHubUtils.fromContentUri(uri);
|
||||
if (!contentInfo) {
|
||||
throw new GitHubContentProviderError(`Failed to parse ${uri}`);
|
||||
}
|
||||
|
||||
const content = Base64Utils.utf8ToB64(stringifyNotebook(toJS(makeNotebookRecord())));
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: false
|
||||
};
|
||||
const name = `Untitled-${new Date().toLocaleString("default", options)}.ipynb`;
|
||||
let path = name;
|
||||
if (contentInfo.path) {
|
||||
path = UrlUtility.createUri(contentInfo.path, name);
|
||||
}
|
||||
|
||||
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
||||
contentInfo.owner,
|
||||
contentInfo.repo,
|
||||
contentInfo.branch,
|
||||
path,
|
||||
commitMsg,
|
||||
content
|
||||
);
|
||||
if (response.status !== HttpStatusCodes.Created) {
|
||||
throw new GitHubContentProviderError("Failed to create", response.status);
|
||||
}
|
||||
|
||||
const newUri = GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path);
|
||||
const newGitHubFile: IGitHubFile = {
|
||||
type: "blob",
|
||||
name: NotebookUtil.getName(newUri),
|
||||
path,
|
||||
repo: {
|
||||
owner: contentInfo.owner,
|
||||
name: contentInfo.repo,
|
||||
private: undefined
|
||||
},
|
||||
branch: {
|
||||
name: contentInfo.branch
|
||||
},
|
||||
commit: response.data
|
||||
};
|
||||
|
||||
return this.createSuccessAjaxResponse(
|
||||
HttpStatusCodes.Created,
|
||||
this.createContentModel(newUri, newGitHubFile, { content: 0 })
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubContentProvider/create", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public save<FT extends FileType>(
|
||||
_: ServerConfig,
|
||||
uri: string,
|
||||
model: Partial<IContent<FT>>
|
||||
): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
let commitMsg: string;
|
||||
if (content.status === HttpStatusCodes.NotFound) {
|
||||
// We'll create a new file since it doesn't exist
|
||||
commitMsg = await this.params.promptForCommitMsg("Save", "Save");
|
||||
if (!commitMsg) {
|
||||
throw new GitHubContentProviderError("Couldn't get a commit message");
|
||||
}
|
||||
} else {
|
||||
commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save");
|
||||
}
|
||||
|
||||
let updatedContent: string;
|
||||
if (model.type === "notebook") {
|
||||
updatedContent = Base64Utils.utf8ToB64(stringifyNotebook(model.content as Notebook));
|
||||
} else if (model.type === "file") {
|
||||
updatedContent = model.content as string;
|
||||
if (model.format !== "base64") {
|
||||
updatedContent = Base64Utils.utf8ToB64(updatedContent);
|
||||
}
|
||||
} else {
|
||||
throw new GitHubContentProviderError("Unsupported content type");
|
||||
}
|
||||
|
||||
const contentInfo = GitHubUtils.fromContentUri(uri);
|
||||
let gitHubFile: IGitHubFile;
|
||||
if (content.data) {
|
||||
gitHubFile = content.data as IGitHubFile;
|
||||
}
|
||||
|
||||
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
||||
contentInfo.owner,
|
||||
contentInfo.repo,
|
||||
contentInfo.branch,
|
||||
contentInfo.path,
|
||||
commitMsg,
|
||||
updatedContent,
|
||||
gitHubFile?.sha
|
||||
);
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.Created) {
|
||||
throw new GitHubContentProviderError("Failed to create or update", response.status);
|
||||
}
|
||||
|
||||
if (gitHubFile) {
|
||||
gitHubFile.commit = response.data;
|
||||
} else {
|
||||
const contentResponse = await this.params.gitHubClient.getContentsAsync(
|
||||
contentInfo.owner,
|
||||
contentInfo.repo,
|
||||
contentInfo.branch,
|
||||
contentInfo.path
|
||||
);
|
||||
if (contentResponse.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get content", response.status);
|
||||
}
|
||||
|
||||
gitHubFile = contentResponse.data as IGitHubFile;
|
||||
}
|
||||
|
||||
return this.createSuccessAjaxResponse(
|
||||
HttpStatusCodes.OK,
|
||||
this.createContentModel(uri, gitHubFile, { content: 0 })
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public listCheckpoints(_: ServerConfig, path: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error.message, "GitHubContentProvider/listCheckpoints", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
public createCheckpoint(_: ServerConfig, path: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error.message, "GitHubContentProvider/createCheckpoint", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
public deleteCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error.message, "GitHubContentProvider/deleteCheckpoint", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
public restoreFromCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
private async validateContentAndGetCommitMsg(
|
||||
content: IGitHubResponse<IGitHubFile | IGitHubFile[]>,
|
||||
promptTitle: string,
|
||||
promptPrimaryButtonLabel: string
|
||||
): Promise<string> {
|
||||
if (content.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get content", content.status);
|
||||
}
|
||||
|
||||
if (Array.isArray(content.data)) {
|
||||
throw new GitHubContentProviderError("Operation not supported for collections");
|
||||
}
|
||||
|
||||
const commitMsg = await this.params.promptForCommitMsg(promptTitle, promptPrimaryButtonLabel);
|
||||
if (!commitMsg) {
|
||||
throw new GitHubContentProviderError("Couldn't get a commit message");
|
||||
}
|
||||
|
||||
return commitMsg;
|
||||
}
|
||||
|
||||
private async getContent(uri: string): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
|
||||
const contentInfo = GitHubUtils.fromContentUri(uri);
|
||||
if (contentInfo) {
|
||||
const { owner, repo, branch, path } = contentInfo;
|
||||
return this.params.gitHubClient.getContentsAsync(owner, repo, branch, path);
|
||||
}
|
||||
|
||||
return Promise.resolve({ status: GitHubContentProvider.SelfErrorCode, data: undefined });
|
||||
}
|
||||
|
||||
private createContentModel(
|
||||
uri: string,
|
||||
content: IGitHubFile | IGitHubFile[],
|
||||
params: Partial<IGetParams>
|
||||
): IContent<FileType> {
|
||||
if (Array.isArray(content)) {
|
||||
return this.createDirectoryModel(uri, content);
|
||||
}
|
||||
|
||||
if (content.type === "tree") {
|
||||
return this.createDirectoryModel(uri, undefined);
|
||||
}
|
||||
|
||||
if (NotebookUtil.isNotebookFile(uri)) {
|
||||
return this.createNotebookModel(content, params);
|
||||
}
|
||||
|
||||
return this.createFileModel(content, params);
|
||||
}
|
||||
|
||||
private createDirectoryModel(uri: string, gitHubFiles: IGitHubFile[] | undefined): IContent<"directory"> {
|
||||
return {
|
||||
name: NotebookUtil.getName(uri),
|
||||
path: uri,
|
||||
type: "directory",
|
||||
writable: true, // TODO: tamitta: we don't know this info here
|
||||
created: "", // TODO: tamitta: we don't know this info here
|
||||
last_modified: "", // TODO: tamitta: we don't know this info here
|
||||
mimetype: undefined,
|
||||
content: gitHubFiles?.map(
|
||||
(file: IGitHubFile) =>
|
||||
this.createContentModel(
|
||||
GitHubUtils.toContentUri(file.repo.owner, file.repo.name, file.branch.name, file.path),
|
||||
file,
|
||||
{
|
||||
content: 0
|
||||
}
|
||||
) as IEmptyContent<FileType>
|
||||
),
|
||||
format: "json"
|
||||
};
|
||||
}
|
||||
|
||||
private createNotebookModel(gitHubFile: IGitHubFile, params: Partial<IGetParams>): IContent<"notebook"> {
|
||||
const content: Notebook = gitHubFile.content && params.content !== 0 ? JSON.parse(gitHubFile.content) : undefined;
|
||||
return {
|
||||
name: gitHubFile.name,
|
||||
path: GitHubUtils.toContentUri(
|
||||
gitHubFile.repo.owner,
|
||||
gitHubFile.repo.name,
|
||||
gitHubFile.branch.name,
|
||||
gitHubFile.path
|
||||
),
|
||||
type: "notebook",
|
||||
writable: true, // TODO: tamitta: we don't know this info here
|
||||
created: "", // TODO: tamitta: we don't know this info here
|
||||
last_modified: gitHubFile.commit.commitDate,
|
||||
mimetype: content ? "application/x-ipynb+json" : undefined,
|
||||
content,
|
||||
format: content ? "json" : undefined
|
||||
};
|
||||
}
|
||||
|
||||
private createFileModel(gitHubFile: IGitHubFile, params: Partial<IGetParams>): IContent<"file"> {
|
||||
const content: string = gitHubFile.content && params.content !== 0 ? gitHubFile.content : undefined;
|
||||
return {
|
||||
name: gitHubFile.name,
|
||||
path: GitHubUtils.toContentUri(
|
||||
gitHubFile.repo.owner,
|
||||
gitHubFile.repo.name,
|
||||
gitHubFile.branch.name,
|
||||
gitHubFile.path
|
||||
),
|
||||
type: "file",
|
||||
writable: true, // TODO: tamitta: we don't know this info here
|
||||
created: "", // TODO: tamitta: we don't know this info here
|
||||
last_modified: gitHubFile.commit.commitDate,
|
||||
mimetype: content ? "text/plain" : undefined,
|
||||
content,
|
||||
format: content ? "text" : undefined
|
||||
};
|
||||
}
|
||||
|
||||
private createSuccessAjaxResponse(status: number, content: IContent<FileType>): AjaxResponse {
|
||||
return {
|
||||
originalEvent: new Event("no-op"),
|
||||
xhr: new XMLHttpRequest(),
|
||||
request: {},
|
||||
status,
|
||||
response: content ? content : undefined,
|
||||
responseText: content ? JSON.stringify(content) : undefined,
|
||||
responseType: "json"
|
||||
};
|
||||
}
|
||||
|
||||
private createErrorAjaxResponse(error: GitHubContentProviderError): AjaxResponse {
|
||||
return {
|
||||
originalEvent: new Event("no-op"),
|
||||
xhr: new XMLHttpRequest(),
|
||||
request: {},
|
||||
status: error.errno,
|
||||
response: error,
|
||||
responseText: getErrorMessage(error),
|
||||
responseType: "json"
|
||||
};
|
||||
}
|
||||
}
|
||||
import { Notebook, stringifyNotebook, makeNotebookRecord, toJS } from "@nteract/commutable";
|
||||
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
||||
import { from, Observable, of } from "rxjs";
|
||||
import { AjaxResponse } from "rxjs/ajax";
|
||||
import * as Base64Utils from "../Utils/Base64Utils";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
||||
import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient";
|
||||
import * as GitHubUtils from "../Utils/GitHubUtils";
|
||||
import UrlUtility from "../Common/UrlUtility";
|
||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||
|
||||
export interface GitHubContentProviderParams {
|
||||
gitHubClient: GitHubClient;
|
||||
promptForCommitMsg: (title: string, primaryButtonLabel: string) => Promise<string>;
|
||||
}
|
||||
|
||||
class GitHubContentProviderError extends Error {
|
||||
constructor(error: string, public errno: number = GitHubContentProvider.SelfErrorCode) {
|
||||
super(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Provides 'contents' API for GitHub
|
||||
// http://jupyter-api.surge.sh/#!/contents
|
||||
export class GitHubContentProvider implements IContentProvider {
|
||||
public static readonly SelfErrorCode = 555;
|
||||
|
||||
constructor(private params: GitHubContentProviderParams) {}
|
||||
|
||||
public remove(_: ServerConfig, uri: string): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Delete", "Delete");
|
||||
const response = await this.params.gitHubClient.deleteFileAsync(content.data as IGitHubFile, commitMsg);
|
||||
if (response.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to delete", response.status);
|
||||
}
|
||||
|
||||
return this.createSuccessAjaxResponse(HttpStatusCodes.NoContent, undefined);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubContentProvider/remove", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public get(_: ServerConfig, uri: string, params: Partial<IGetParams>): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
if (content.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get content", content.status);
|
||||
}
|
||||
|
||||
if (!Array.isArray(content.data) && !content.data.content && params.content !== 0) {
|
||||
const file = content.data;
|
||||
file.content = (
|
||||
await this.params.gitHubClient.getBlobAsync(file.repo.owner, file.repo.name, file.sha)
|
||||
).data;
|
||||
}
|
||||
|
||||
return this.createSuccessAjaxResponse(HttpStatusCodes.OK, this.createContentModel(uri, content.data, params));
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubContentProvider/get", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public update<FT extends FileType>(
|
||||
_: ServerConfig,
|
||||
uri: string,
|
||||
model: Partial<IContent<FT>>
|
||||
): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
const gitHubFile = content.data as IGitHubFile;
|
||||
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Rename", "Rename");
|
||||
const newUri = model.path;
|
||||
const newPath = GitHubUtils.fromContentUri(newUri).path;
|
||||
const response = await this.params.gitHubClient.renameFileAsync(
|
||||
gitHubFile.repo.owner,
|
||||
gitHubFile.repo.name,
|
||||
gitHubFile.branch.name,
|
||||
commitMsg,
|
||||
gitHubFile.path,
|
||||
newPath
|
||||
);
|
||||
if (response.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to rename", response.status);
|
||||
}
|
||||
|
||||
gitHubFile.commit = response.data;
|
||||
gitHubFile.path = newPath;
|
||||
gitHubFile.name = NotebookUtil.getName(gitHubFile.path);
|
||||
|
||||
return this.createSuccessAjaxResponse(
|
||||
HttpStatusCodes.OK,
|
||||
this.createContentModel(newUri, gitHubFile, { content: 0 })
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public create<FT extends FileType>(
|
||||
_: ServerConfig,
|
||||
uri: string,
|
||||
model: Partial<IContent<FT>> & { type: FT }
|
||||
): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.params.promptForCommitMsg("Create New Notebook", "Create").then(async (commitMsg: string) => {
|
||||
try {
|
||||
if (!commitMsg) {
|
||||
throw new GitHubContentProviderError("Couldn't get a commit message");
|
||||
}
|
||||
|
||||
if (model.type !== "notebook") {
|
||||
throw new GitHubContentProviderError("Unsupported content type");
|
||||
}
|
||||
|
||||
const contentInfo = GitHubUtils.fromContentUri(uri);
|
||||
if (!contentInfo) {
|
||||
throw new GitHubContentProviderError(`Failed to parse ${uri}`);
|
||||
}
|
||||
|
||||
const content = Base64Utils.utf8ToB64(stringifyNotebook(toJS(makeNotebookRecord())));
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: false,
|
||||
};
|
||||
const name = `Untitled-${new Date().toLocaleString("default", options)}.ipynb`;
|
||||
let path = name;
|
||||
if (contentInfo.path) {
|
||||
path = UrlUtility.createUri(contentInfo.path, name);
|
||||
}
|
||||
|
||||
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
||||
contentInfo.owner,
|
||||
contentInfo.repo,
|
||||
contentInfo.branch,
|
||||
path,
|
||||
commitMsg,
|
||||
content
|
||||
);
|
||||
if (response.status !== HttpStatusCodes.Created) {
|
||||
throw new GitHubContentProviderError("Failed to create", response.status);
|
||||
}
|
||||
|
||||
const newUri = GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path);
|
||||
const newGitHubFile: IGitHubFile = {
|
||||
type: "blob",
|
||||
name: NotebookUtil.getName(newUri),
|
||||
path,
|
||||
repo: {
|
||||
owner: contentInfo.owner,
|
||||
name: contentInfo.repo,
|
||||
private: undefined,
|
||||
},
|
||||
branch: {
|
||||
name: contentInfo.branch,
|
||||
},
|
||||
commit: response.data,
|
||||
};
|
||||
|
||||
return this.createSuccessAjaxResponse(
|
||||
HttpStatusCodes.Created,
|
||||
this.createContentModel(newUri, newGitHubFile, { content: 0 })
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubContentProvider/create", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public save<FT extends FileType>(
|
||||
_: ServerConfig,
|
||||
uri: string,
|
||||
model: Partial<IContent<FT>>
|
||||
): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
let commitMsg: string;
|
||||
if (content.status === HttpStatusCodes.NotFound) {
|
||||
// We'll create a new file since it doesn't exist
|
||||
commitMsg = await this.params.promptForCommitMsg("Save", "Save");
|
||||
if (!commitMsg) {
|
||||
throw new GitHubContentProviderError("Couldn't get a commit message");
|
||||
}
|
||||
} else {
|
||||
commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save");
|
||||
}
|
||||
|
||||
let updatedContent: string;
|
||||
if (model.type === "notebook") {
|
||||
updatedContent = Base64Utils.utf8ToB64(stringifyNotebook(model.content as Notebook));
|
||||
} else if (model.type === "file") {
|
||||
updatedContent = model.content as string;
|
||||
if (model.format !== "base64") {
|
||||
updatedContent = Base64Utils.utf8ToB64(updatedContent);
|
||||
}
|
||||
} else {
|
||||
throw new GitHubContentProviderError("Unsupported content type");
|
||||
}
|
||||
|
||||
const contentInfo = GitHubUtils.fromContentUri(uri);
|
||||
let gitHubFile: IGitHubFile;
|
||||
if (content.data) {
|
||||
gitHubFile = content.data as IGitHubFile;
|
||||
}
|
||||
|
||||
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
||||
contentInfo.owner,
|
||||
contentInfo.repo,
|
||||
contentInfo.branch,
|
||||
contentInfo.path,
|
||||
commitMsg,
|
||||
updatedContent,
|
||||
gitHubFile?.sha
|
||||
);
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.Created) {
|
||||
throw new GitHubContentProviderError("Failed to create or update", response.status);
|
||||
}
|
||||
|
||||
if (gitHubFile) {
|
||||
gitHubFile.commit = response.data;
|
||||
} else {
|
||||
const contentResponse = await this.params.gitHubClient.getContentsAsync(
|
||||
contentInfo.owner,
|
||||
contentInfo.repo,
|
||||
contentInfo.branch,
|
||||
contentInfo.path
|
||||
);
|
||||
if (contentResponse.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get content", response.status);
|
||||
}
|
||||
|
||||
gitHubFile = contentResponse.data as IGitHubFile;
|
||||
}
|
||||
|
||||
return this.createSuccessAjaxResponse(
|
||||
HttpStatusCodes.OK,
|
||||
this.createContentModel(uri, gitHubFile, { content: 0 })
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public listCheckpoints(_: ServerConfig, path: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error.message, "GitHubContentProvider/listCheckpoints", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
public createCheckpoint(_: ServerConfig, path: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error.message, "GitHubContentProvider/createCheckpoint", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
public deleteCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error.message, "GitHubContentProvider/deleteCheckpoint", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
public restoreFromCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
private async validateContentAndGetCommitMsg(
|
||||
content: IGitHubResponse<IGitHubFile | IGitHubFile[]>,
|
||||
promptTitle: string,
|
||||
promptPrimaryButtonLabel: string
|
||||
): Promise<string> {
|
||||
if (content.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get content", content.status);
|
||||
}
|
||||
|
||||
if (Array.isArray(content.data)) {
|
||||
throw new GitHubContentProviderError("Operation not supported for collections");
|
||||
}
|
||||
|
||||
const commitMsg = await this.params.promptForCommitMsg(promptTitle, promptPrimaryButtonLabel);
|
||||
if (!commitMsg) {
|
||||
throw new GitHubContentProviderError("Couldn't get a commit message");
|
||||
}
|
||||
|
||||
return commitMsg;
|
||||
}
|
||||
|
||||
private async getContent(uri: string): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
|
||||
const contentInfo = GitHubUtils.fromContentUri(uri);
|
||||
if (contentInfo) {
|
||||
const { owner, repo, branch, path } = contentInfo;
|
||||
return this.params.gitHubClient.getContentsAsync(owner, repo, branch, path);
|
||||
}
|
||||
|
||||
return Promise.resolve({ status: GitHubContentProvider.SelfErrorCode, data: undefined });
|
||||
}
|
||||
|
||||
private createContentModel(
|
||||
uri: string,
|
||||
content: IGitHubFile | IGitHubFile[],
|
||||
params: Partial<IGetParams>
|
||||
): IContent<FileType> {
|
||||
if (Array.isArray(content)) {
|
||||
return this.createDirectoryModel(uri, content);
|
||||
}
|
||||
|
||||
if (content.type === "tree") {
|
||||
return this.createDirectoryModel(uri, undefined);
|
||||
}
|
||||
|
||||
if (NotebookUtil.isNotebookFile(uri)) {
|
||||
return this.createNotebookModel(content, params);
|
||||
}
|
||||
|
||||
return this.createFileModel(content, params);
|
||||
}
|
||||
|
||||
private createDirectoryModel(uri: string, gitHubFiles: IGitHubFile[] | undefined): IContent<"directory"> {
|
||||
return {
|
||||
name: NotebookUtil.getName(uri),
|
||||
path: uri,
|
||||
type: "directory",
|
||||
writable: true, // TODO: tamitta: we don't know this info here
|
||||
created: "", // TODO: tamitta: we don't know this info here
|
||||
last_modified: "", // TODO: tamitta: we don't know this info here
|
||||
mimetype: undefined,
|
||||
content: gitHubFiles?.map(
|
||||
(file: IGitHubFile) =>
|
||||
this.createContentModel(
|
||||
GitHubUtils.toContentUri(file.repo.owner, file.repo.name, file.branch.name, file.path),
|
||||
file,
|
||||
{
|
||||
content: 0,
|
||||
}
|
||||
) as IEmptyContent<FileType>
|
||||
),
|
||||
format: "json",
|
||||
};
|
||||
}
|
||||
|
||||
private createNotebookModel(gitHubFile: IGitHubFile, params: Partial<IGetParams>): IContent<"notebook"> {
|
||||
const content: Notebook = gitHubFile.content && params.content !== 0 ? JSON.parse(gitHubFile.content) : undefined;
|
||||
return {
|
||||
name: gitHubFile.name,
|
||||
path: GitHubUtils.toContentUri(
|
||||
gitHubFile.repo.owner,
|
||||
gitHubFile.repo.name,
|
||||
gitHubFile.branch.name,
|
||||
gitHubFile.path
|
||||
),
|
||||
type: "notebook",
|
||||
writable: true, // TODO: tamitta: we don't know this info here
|
||||
created: "", // TODO: tamitta: we don't know this info here
|
||||
last_modified: gitHubFile.commit.commitDate,
|
||||
mimetype: content ? "application/x-ipynb+json" : undefined,
|
||||
content,
|
||||
format: content ? "json" : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private createFileModel(gitHubFile: IGitHubFile, params: Partial<IGetParams>): IContent<"file"> {
|
||||
const content: string = gitHubFile.content && params.content !== 0 ? gitHubFile.content : undefined;
|
||||
return {
|
||||
name: gitHubFile.name,
|
||||
path: GitHubUtils.toContentUri(
|
||||
gitHubFile.repo.owner,
|
||||
gitHubFile.repo.name,
|
||||
gitHubFile.branch.name,
|
||||
gitHubFile.path
|
||||
),
|
||||
type: "file",
|
||||
writable: true, // TODO: tamitta: we don't know this info here
|
||||
created: "", // TODO: tamitta: we don't know this info here
|
||||
last_modified: gitHubFile.commit.commitDate,
|
||||
mimetype: content ? "text/plain" : undefined,
|
||||
content,
|
||||
format: content ? "text" : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private createSuccessAjaxResponse(status: number, content: IContent<FileType>): AjaxResponse {
|
||||
return {
|
||||
originalEvent: new Event("no-op"),
|
||||
xhr: new XMLHttpRequest(),
|
||||
request: {},
|
||||
status,
|
||||
response: content ? content : undefined,
|
||||
responseText: content ? JSON.stringify(content) : undefined,
|
||||
responseType: "json",
|
||||
};
|
||||
}
|
||||
|
||||
private createErrorAjaxResponse(error: GitHubContentProviderError): AjaxResponse {
|
||||
return {
|
||||
originalEvent: new Event("no-op"),
|
||||
xhr: new XMLHttpRequest(),
|
||||
request: {},
|
||||
status: error.errno,
|
||||
response: error,
|
||||
responseText: getErrorMessage(error),
|
||||
responseType: "json",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,166 +1,166 @@
|
||||
import ko from "knockout";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { GitHubConnector, IGitHubConnectorParams } from "./GitHubConnector";
|
||||
import { GitHubOAuthService } from "./GitHubOAuthService";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import NotebookManager from "../Explorer/Notebook/NotebookManager";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
|
||||
const sampleDatabaseAccount: DataModels.DatabaseAccount = {
|
||||
id: "id",
|
||||
name: "name",
|
||||
location: "location",
|
||||
type: "type",
|
||||
kind: "kind",
|
||||
tags: [],
|
||||
properties: {
|
||||
documentEndpoint: "documentEndpoint",
|
||||
gremlinEndpoint: "gremlinEndpoint",
|
||||
tableEndpoint: "tableEndpoint",
|
||||
cassandraEndpoint: "cassandraEndpoint"
|
||||
}
|
||||
};
|
||||
|
||||
describe("GitHubOAuthService", () => {
|
||||
let junoClient: JunoClient;
|
||||
let gitHubOAuthService: GitHubOAuthService;
|
||||
let originalDataExplorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
junoClient = new JunoClient(ko.observable<DataModels.DatabaseAccount>(sampleDatabaseAccount));
|
||||
gitHubOAuthService = new GitHubOAuthService(junoClient);
|
||||
originalDataExplorer = window.dataExplorer;
|
||||
window.dataExplorer = {
|
||||
...originalDataExplorer,
|
||||
logConsoleData: (data): void =>
|
||||
data.type === ConsoleDataType.Error ? console.error(data.message) : console.error(data.message)
|
||||
} as Explorer;
|
||||
window.dataExplorer.notebookManager = new NotebookManager();
|
||||
window.dataExplorer.notebookManager.junoClient = junoClient;
|
||||
window.dataExplorer.notebookManager.gitHubOAuthService = gitHubOAuthService;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
window.dataExplorer = originalDataExplorer;
|
||||
originalDataExplorer = undefined;
|
||||
gitHubOAuthService = undefined;
|
||||
junoClient = undefined;
|
||||
});
|
||||
|
||||
it("logout deletes app authorization and resets token", async () => {
|
||||
const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent });
|
||||
junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback;
|
||||
|
||||
await gitHubOAuthService.logout();
|
||||
expect(deleteAppAuthorizationCallback).toBeCalled();
|
||||
expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resetToken resets token", () => {
|
||||
gitHubOAuthService.resetToken();
|
||||
expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("startOAuth resets OAuth state", async () => {
|
||||
let url: string;
|
||||
const windowOpenCallback = jest.fn().mockImplementation((value: string) => {
|
||||
url = value;
|
||||
});
|
||||
window.open = windowOpenCallback;
|
||||
|
||||
await gitHubOAuthService.startOAuth("scope");
|
||||
expect(windowOpenCallback).toBeCalled();
|
||||
|
||||
const initialParams = new URLSearchParams(new URL(url).search);
|
||||
expect(initialParams.get("state")).toBeDefined();
|
||||
|
||||
await gitHubOAuthService.startOAuth("another scope");
|
||||
expect(windowOpenCallback).toBeCalled();
|
||||
|
||||
const newParams = new URLSearchParams(new URL(url).search);
|
||||
expect(newParams.get("state")).toBeDefined();
|
||||
expect(newParams.get("state")).not.toEqual(initialParams.get("state"));
|
||||
});
|
||||
|
||||
it("finishOAuth is called whenever GitHubConnector is started", async () => {
|
||||
const finishOAuthCallback = jest.fn().mockImplementation();
|
||||
gitHubOAuthService.finishOAuth = finishOAuthCallback;
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state: "state",
|
||||
code: "code"
|
||||
};
|
||||
const searchParams = new URLSearchParams({ ...params });
|
||||
|
||||
const gitHubConnector = new GitHubConnector();
|
||||
gitHubConnector.start(searchParams, window);
|
||||
|
||||
// GitHubConnector uses Window.postMessage and there's no good way to know when the message has received
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(finishOAuthCallback).toBeCalledWith(params);
|
||||
});
|
||||
|
||||
it("finishOAuth updates token", async () => {
|
||||
const data = { key: "value" };
|
||||
const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, data });
|
||||
junoClient.getGitHubToken = getGitHubTokenCallback;
|
||||
|
||||
const initialToken = gitHubOAuthService.getTokenObservable()();
|
||||
const state = await gitHubOAuthService.startOAuth("scope");
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state,
|
||||
code: "code"
|
||||
};
|
||||
await gitHubOAuthService.finishOAuth(params);
|
||||
const updatedToken = gitHubOAuthService.getTokenObservable()();
|
||||
|
||||
expect(getGitHubTokenCallback).toBeCalledWith("code");
|
||||
expect(initialToken).not.toEqual(updatedToken);
|
||||
});
|
||||
|
||||
it("finishOAuth updates token to error if state doesn't match", async () => {
|
||||
await gitHubOAuthService.startOAuth("scope");
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state: "state",
|
||||
code: "code"
|
||||
};
|
||||
await gitHubOAuthService.finishOAuth(params);
|
||||
|
||||
expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined();
|
||||
});
|
||||
|
||||
it("finishOAuth updates token to error if unable to fetch token", async () => {
|
||||
const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NotFound });
|
||||
junoClient.getGitHubToken = getGitHubTokenCallback;
|
||||
|
||||
const state = await gitHubOAuthService.startOAuth("scope");
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state,
|
||||
code: "code"
|
||||
};
|
||||
await gitHubOAuthService.finishOAuth(params);
|
||||
|
||||
expect(getGitHubTokenCallback).toBeCalledWith("code");
|
||||
expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined();
|
||||
});
|
||||
|
||||
it("isLoggedIn returns false if resetToken is called", () => {
|
||||
gitHubOAuthService.resetToken();
|
||||
expect(gitHubOAuthService.isLoggedIn()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("isLoggedIn returns false if logout is called", async () => {
|
||||
const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent });
|
||||
junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback;
|
||||
|
||||
await gitHubOAuthService.logout();
|
||||
expect(gitHubOAuthService.isLoggedIn()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
import ko from "knockout";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { GitHubConnector, IGitHubConnectorParams } from "./GitHubConnector";
|
||||
import { GitHubOAuthService } from "./GitHubOAuthService";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import NotebookManager from "../Explorer/Notebook/NotebookManager";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
|
||||
const sampleDatabaseAccount: DataModels.DatabaseAccount = {
|
||||
id: "id",
|
||||
name: "name",
|
||||
location: "location",
|
||||
type: "type",
|
||||
kind: "kind",
|
||||
tags: [],
|
||||
properties: {
|
||||
documentEndpoint: "documentEndpoint",
|
||||
gremlinEndpoint: "gremlinEndpoint",
|
||||
tableEndpoint: "tableEndpoint",
|
||||
cassandraEndpoint: "cassandraEndpoint",
|
||||
},
|
||||
};
|
||||
|
||||
describe("GitHubOAuthService", () => {
|
||||
let junoClient: JunoClient;
|
||||
let gitHubOAuthService: GitHubOAuthService;
|
||||
let originalDataExplorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
junoClient = new JunoClient(ko.observable<DataModels.DatabaseAccount>(sampleDatabaseAccount));
|
||||
gitHubOAuthService = new GitHubOAuthService(junoClient);
|
||||
originalDataExplorer = window.dataExplorer;
|
||||
window.dataExplorer = {
|
||||
...originalDataExplorer,
|
||||
logConsoleData: (data): void =>
|
||||
data.type === ConsoleDataType.Error ? console.error(data.message) : console.error(data.message),
|
||||
} as Explorer;
|
||||
window.dataExplorer.notebookManager = new NotebookManager();
|
||||
window.dataExplorer.notebookManager.junoClient = junoClient;
|
||||
window.dataExplorer.notebookManager.gitHubOAuthService = gitHubOAuthService;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
window.dataExplorer = originalDataExplorer;
|
||||
originalDataExplorer = undefined;
|
||||
gitHubOAuthService = undefined;
|
||||
junoClient = undefined;
|
||||
});
|
||||
|
||||
it("logout deletes app authorization and resets token", async () => {
|
||||
const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent });
|
||||
junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback;
|
||||
|
||||
await gitHubOAuthService.logout();
|
||||
expect(deleteAppAuthorizationCallback).toBeCalled();
|
||||
expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resetToken resets token", () => {
|
||||
gitHubOAuthService.resetToken();
|
||||
expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("startOAuth resets OAuth state", async () => {
|
||||
let url: string;
|
||||
const windowOpenCallback = jest.fn().mockImplementation((value: string) => {
|
||||
url = value;
|
||||
});
|
||||
window.open = windowOpenCallback;
|
||||
|
||||
await gitHubOAuthService.startOAuth("scope");
|
||||
expect(windowOpenCallback).toBeCalled();
|
||||
|
||||
const initialParams = new URLSearchParams(new URL(url).search);
|
||||
expect(initialParams.get("state")).toBeDefined();
|
||||
|
||||
await gitHubOAuthService.startOAuth("another scope");
|
||||
expect(windowOpenCallback).toBeCalled();
|
||||
|
||||
const newParams = new URLSearchParams(new URL(url).search);
|
||||
expect(newParams.get("state")).toBeDefined();
|
||||
expect(newParams.get("state")).not.toEqual(initialParams.get("state"));
|
||||
});
|
||||
|
||||
it("finishOAuth is called whenever GitHubConnector is started", async () => {
|
||||
const finishOAuthCallback = jest.fn().mockImplementation();
|
||||
gitHubOAuthService.finishOAuth = finishOAuthCallback;
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state: "state",
|
||||
code: "code",
|
||||
};
|
||||
const searchParams = new URLSearchParams({ ...params });
|
||||
|
||||
const gitHubConnector = new GitHubConnector();
|
||||
gitHubConnector.start(searchParams, window);
|
||||
|
||||
// GitHubConnector uses Window.postMessage and there's no good way to know when the message has received
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(finishOAuthCallback).toBeCalledWith(params);
|
||||
});
|
||||
|
||||
it("finishOAuth updates token", async () => {
|
||||
const data = { key: "value" };
|
||||
const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, data });
|
||||
junoClient.getGitHubToken = getGitHubTokenCallback;
|
||||
|
||||
const initialToken = gitHubOAuthService.getTokenObservable()();
|
||||
const state = await gitHubOAuthService.startOAuth("scope");
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state,
|
||||
code: "code",
|
||||
};
|
||||
await gitHubOAuthService.finishOAuth(params);
|
||||
const updatedToken = gitHubOAuthService.getTokenObservable()();
|
||||
|
||||
expect(getGitHubTokenCallback).toBeCalledWith("code");
|
||||
expect(initialToken).not.toEqual(updatedToken);
|
||||
});
|
||||
|
||||
it("finishOAuth updates token to error if state doesn't match", async () => {
|
||||
await gitHubOAuthService.startOAuth("scope");
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state: "state",
|
||||
code: "code",
|
||||
};
|
||||
await gitHubOAuthService.finishOAuth(params);
|
||||
|
||||
expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined();
|
||||
});
|
||||
|
||||
it("finishOAuth updates token to error if unable to fetch token", async () => {
|
||||
const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NotFound });
|
||||
junoClient.getGitHubToken = getGitHubTokenCallback;
|
||||
|
||||
const state = await gitHubOAuthService.startOAuth("scope");
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state,
|
||||
code: "code",
|
||||
};
|
||||
await gitHubOAuthService.finishOAuth(params);
|
||||
|
||||
expect(getGitHubTokenCallback).toBeCalledWith("code");
|
||||
expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined();
|
||||
});
|
||||
|
||||
it("isLoggedIn returns false if resetToken is called", () => {
|
||||
gitHubOAuthService.resetToken();
|
||||
expect(gitHubOAuthService.isLoggedIn()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("isLoggedIn returns false if logout is called", async () => {
|
||||
const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent });
|
||||
junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback;
|
||||
|
||||
await gitHubOAuthService.logout();
|
||||
expect(gitHubOAuthService.isLoggedIn()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,125 +1,125 @@
|
||||
import ko from "knockout";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector";
|
||||
import { handleError } from "../Common/ErrorHandlingUtils";
|
||||
|
||||
window.addEventListener("message", (event: MessageEvent) => {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = event.data;
|
||||
if (msg.type === GitHubConnectorMsgType) {
|
||||
const params = msg.data as IGitHubConnectorParams;
|
||||
window.dataExplorer.notebookManager?.gitHubOAuthService.finishOAuth(params);
|
||||
}
|
||||
});
|
||||
|
||||
export interface IGitHubOAuthToken {
|
||||
// API properties
|
||||
access_token?: string;
|
||||
scope?: string;
|
||||
token_type?: string;
|
||||
error?: string;
|
||||
error_description?: string;
|
||||
}
|
||||
|
||||
export class GitHubOAuthService {
|
||||
private static readonly OAuthEndpoint = "https://github.com/login/oauth/authorize";
|
||||
|
||||
private state: string;
|
||||
private token: ko.Observable<IGitHubOAuthToken>;
|
||||
|
||||
constructor(private junoClient: JunoClient) {
|
||||
this.token = ko.observable<IGitHubOAuthToken>();
|
||||
}
|
||||
|
||||
public async startOAuth(scope: string): Promise<string> {
|
||||
// If attempting to change scope from "Public & private repos" to "Public only" we need to delete app authorization.
|
||||
// Otherwise OAuth app still retains the "public & private repos" permissions.
|
||||
if (
|
||||
this.token()?.scope === AuthorizeAccessComponent.Scopes.PublicAndPrivate.key &&
|
||||
scope === AuthorizeAccessComponent.Scopes.Public.key
|
||||
) {
|
||||
const logoutSuccessful = await this.logout();
|
||||
if (!logoutSuccessful) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const params = {
|
||||
scope,
|
||||
client_id: configContext.GITHUB_CLIENT_ID,
|
||||
redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
|
||||
state: this.resetState()
|
||||
};
|
||||
|
||||
window.open(`${GitHubOAuthService.OAuthEndpoint}?${new URLSearchParams(params).toString()}`);
|
||||
return params.state;
|
||||
}
|
||||
|
||||
public async finishOAuth(params: IGitHubConnectorParams) {
|
||||
try {
|
||||
this.validateState(params.state);
|
||||
const response = await this.junoClient.getGitHubToken(params.code);
|
||||
|
||||
if (response.status === HttpStatusCodes.OK && !response.data.error) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully connected to GitHub");
|
||||
this.token(response.data);
|
||||
} else {
|
||||
let errorMsg = response.data.error;
|
||||
if (response.data.error_description) {
|
||||
errorMsg = `${errorMsg}: ${response.data.error_description}`;
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect to GitHub: ${error}`);
|
||||
this.token({ error });
|
||||
}
|
||||
}
|
||||
|
||||
public getTokenObservable(): ko.Observable<IGitHubOAuthToken> {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
public async logout(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.junoClient.deleteAppAuthorization(this.token()?.access_token);
|
||||
if (response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status}: ${response.data} when deleting app authorization`);
|
||||
}
|
||||
|
||||
this.resetToken();
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, "GitHubOAuthService/logout", "Failed to delete app authorization");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public isLoggedIn(): boolean {
|
||||
return !!this.token()?.access_token;
|
||||
}
|
||||
|
||||
private resetState(): string {
|
||||
this.state = Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER)).toString();
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public resetToken() {
|
||||
this.token(undefined);
|
||||
}
|
||||
|
||||
private validateState(state: string) {
|
||||
if (state !== this.state) {
|
||||
throw new Error("State didn't match. Possibility of cross-site request forgery attack.");
|
||||
}
|
||||
}
|
||||
}
|
||||
import ko from "knockout";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector";
|
||||
import { handleError } from "../Common/ErrorHandlingUtils";
|
||||
|
||||
window.addEventListener("message", (event: MessageEvent) => {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = event.data;
|
||||
if (msg.type === GitHubConnectorMsgType) {
|
||||
const params = msg.data as IGitHubConnectorParams;
|
||||
window.dataExplorer.notebookManager?.gitHubOAuthService.finishOAuth(params);
|
||||
}
|
||||
});
|
||||
|
||||
export interface IGitHubOAuthToken {
|
||||
// API properties
|
||||
access_token?: string;
|
||||
scope?: string;
|
||||
token_type?: string;
|
||||
error?: string;
|
||||
error_description?: string;
|
||||
}
|
||||
|
||||
export class GitHubOAuthService {
|
||||
private static readonly OAuthEndpoint = "https://github.com/login/oauth/authorize";
|
||||
|
||||
private state: string;
|
||||
private token: ko.Observable<IGitHubOAuthToken>;
|
||||
|
||||
constructor(private junoClient: JunoClient) {
|
||||
this.token = ko.observable<IGitHubOAuthToken>();
|
||||
}
|
||||
|
||||
public async startOAuth(scope: string): Promise<string> {
|
||||
// If attempting to change scope from "Public & private repos" to "Public only" we need to delete app authorization.
|
||||
// Otherwise OAuth app still retains the "public & private repos" permissions.
|
||||
if (
|
||||
this.token()?.scope === AuthorizeAccessComponent.Scopes.PublicAndPrivate.key &&
|
||||
scope === AuthorizeAccessComponent.Scopes.Public.key
|
||||
) {
|
||||
const logoutSuccessful = await this.logout();
|
||||
if (!logoutSuccessful) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const params = {
|
||||
scope,
|
||||
client_id: configContext.GITHUB_CLIENT_ID,
|
||||
redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
|
||||
state: this.resetState(),
|
||||
};
|
||||
|
||||
window.open(`${GitHubOAuthService.OAuthEndpoint}?${new URLSearchParams(params).toString()}`);
|
||||
return params.state;
|
||||
}
|
||||
|
||||
public async finishOAuth(params: IGitHubConnectorParams) {
|
||||
try {
|
||||
this.validateState(params.state);
|
||||
const response = await this.junoClient.getGitHubToken(params.code);
|
||||
|
||||
if (response.status === HttpStatusCodes.OK && !response.data.error) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully connected to GitHub");
|
||||
this.token(response.data);
|
||||
} else {
|
||||
let errorMsg = response.data.error;
|
||||
if (response.data.error_description) {
|
||||
errorMsg = `${errorMsg}: ${response.data.error_description}`;
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect to GitHub: ${error}`);
|
||||
this.token({ error });
|
||||
}
|
||||
}
|
||||
|
||||
public getTokenObservable(): ko.Observable<IGitHubOAuthToken> {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
public async logout(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.junoClient.deleteAppAuthorization(this.token()?.access_token);
|
||||
if (response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status}: ${response.data} when deleting app authorization`);
|
||||
}
|
||||
|
||||
this.resetToken();
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, "GitHubOAuthService/logout", "Failed to delete app authorization");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public isLoggedIn(): boolean {
|
||||
return !!this.token()?.access_token;
|
||||
}
|
||||
|
||||
private resetState(): string {
|
||||
this.state = Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER)).toString();
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public resetToken() {
|
||||
this.token(undefined);
|
||||
}
|
||||
|
||||
private validateState(state: string) {
|
||||
if (state !== this.state) {
|
||||
throw new Error("State didn't match. Possibility of cross-site request forgery attack.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user