Integrate the identity plugin (#1385)

This commit integrates the identity plugin, which was created in #1384.
It does this by adding explicit identity fields to the project
configuration, which are then applied when loading the graph in
`api/load.js`.

The actual integration is quite straightforward.

Test plan: The underlying logic is thoroughly tested; I added one new
test case to verify that it is integrated properly. Since the project
compat has changed, I've updated all the snapshots. Prior to merging
this PR, I will produce one "integration test", using this code to do
identity resolution for a real project (i.e. on the SourceCred instance
itself).
This commit is contained in:
Dandelion Mané 2019-09-20 12:08:27 +02:00 committed by GitHub
parent 9a9f211901
commit 54ece536d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 64 additions and 5 deletions

View File

@ -3,6 +3,7 @@
## [Unreleased]
<!-- Please add new entries just beneath this line. -->
- Enable combining different user identities together (#1385)
- Add `sourcecred discourse` for loading Discourse servers (#1374)
- Breaking: Change output format for the scores command (#1372)
- Include top nodes for every type in Timeline Cred (#1358)

View File

@ -1 +1 @@
[{"type":"sourcecred/project","version":"0.2.0"},{"discourseServer":null,"id":"sourcecred-test/example-github","repoIds":[{"name":"example-github","owner":"sourcecred-test"}]}]
[{"type":"sourcecred/project","version":"0.3.0"},{"discourseServer":null,"id":"sourcecred-test/example-github","identities":[],"repoIds":[{"name":"example-github","owner":"sourcecred-test"}]}]

View File

@ -15,6 +15,7 @@ import {setupProjectDirectory} from "../core/project_io";
import {loadDiscourse} from "../plugins/discourse/loadDiscourse";
import {type PluginDeclaration} from "../analysis/pluginDeclaration";
import * as NullUtil from "../util/null";
import {nodeContractions} from "../plugins/identity/nodeContractions";
export type LoadOptions = {|
+project: Project,
@ -91,7 +92,16 @@ export async function load(
]);
const pluginGraphs = await Promise.all(pluginGraphPromises);
const graph = Graph.merge(pluginGraphs);
let graph = Graph.merge(pluginGraphs);
const {identities, discourseServer} = project;
if (identities.length) {
const serverUrl =
discourseServer == null ? null : discourseServer.serverUrl;
const contractions = nodeContractions(identities, serverUrl);
// Only apply contractions if identities have been specified, since it involves
// a full Graph copy
graph = graph.contractNodes(contractions);
}
const projectDirectory = await setupProjectDirectory(
project,

View File

@ -7,6 +7,7 @@ import fs from "fs-extra";
import type {Options as LoadGraphOptions} from "../plugins/github/loadGraph";
import type {Options as LoadDiscourseOptions} from "../plugins/discourse/loadDiscourse";
import {nodeContractions} from "../plugins/identity/nodeContractions";
import type {Project} from "../core/project";
import {
directoryForProjectId,
@ -66,6 +67,7 @@ describe("api/load", () => {
serverUrl: discourseServerUrl,
apiUsername: discourseApiUsername,
},
identities: [],
};
deepFreeze(project);
const githubToken = "EXAMPLE_TOKEN";
@ -222,4 +224,23 @@ describe("api/load", () => {
const expectedJSON = discourseGraph().toJSON();
expect(graphJSON).toEqual(expectedJSON);
});
it("applies identity transformations, if present in the project", async () => {
const {options, taskReporter, sourcecredDirectory} = example();
const identity = {username: "identity", aliases: []};
const newProject = {...options.project, identities: [identity]};
const newOptions = {...options, project: newProject};
await load(newOptions, taskReporter);
const projectDirectory = directoryForProjectId(
project.id,
sourcecredDirectory
);
const graphFile = path.join(projectDirectory, "graph.json");
const graphJSON = JSON.parse(await fs.readFile(graphFile));
const identityGraph = combinedGraph().contractNodes(
nodeContractions([identity], discourseServerUrl)
);
const expectedJSON = identityGraph.toJSON();
expect(graphJSON).toEqual(expectedJSON);
});
});

View File

@ -105,6 +105,7 @@ const command: Command = async (args, std) => {
id: projectId,
repoIds: [],
discourseServer: {serverUrl, apiUsername},
identities: [],
};
const taskReporter = new LoggingTaskReporter();
let weights = defaultWeights();

View File

@ -171,7 +171,7 @@ export async function createProject(opts: {|
const subproject = await specToProject(spec, NullUtil.get(githubToken));
repoIds = repoIds.concat(subproject.repoIds);
}
return {id: projectId, repoIds, discourseServer};
return {id: projectId, repoIds, discourseServer, identities: []};
}
export default genProject;

View File

@ -14,6 +14,7 @@ import {partialParams} from "../analysis/timeline/params";
import {type PluginDeclaration} from "../analysis/pluginDeclaration";
import {declaration as discourseDeclaration} from "../plugins/discourse/declaration";
import {declaration as githubDeclaration} from "../plugins/github/declaration";
import {declaration as identityDeclaration} from "../plugins/identity/declaration";
function usage(print: (string) => void): void {
print(
@ -132,6 +133,9 @@ const loadCommand: Command = async (args, std) => {
if (project.repoIds.length) {
plugins.push(githubDeclaration);
}
if (project.identities.length) {
plugins.push(identityDeclaration);
}
return {
project,
params,

View File

@ -77,6 +77,7 @@ describe("cli/load", () => {
id: "foo/bar",
repoIds: [makeRepoId("foo", "bar")],
discourseServer: null,
identities: [],
},
params: defaultParams(),
plugins: [githubDeclaration],
@ -102,6 +103,7 @@ describe("cli/load", () => {
id: projectId,
repoIds: [stringToRepoId(projectId)],
discourseServer: null,
identities: [],
},
params: defaultParams(),
plugins: [githubDeclaration],
@ -140,6 +142,7 @@ describe("cli/load", () => {
id: "foo/bar",
repoIds: [makeRepoId("foo", "bar")],
discourseServer: null,
identities: [],
},
params: partialParams({weights}),
plugins: [githubDeclaration],

View File

@ -3,6 +3,7 @@
import base64url from "base64url";
import {type RepoId} from "../core/repoId";
import {toCompat, fromCompat, type Compatible} from "../util/compat";
import {type Identity} from "../plugins/identity/identity";
export type ProjectId = string;
@ -29,9 +30,10 @@ export type Project = {|
+serverUrl: string,
+apiUsername: string,
|} | null,
+identities: $ReadOnlyArray<Identity>,
|};
const COMPAT_INFO = {type: "sourcecred/project", version: "0.2.0"};
const COMPAT_INFO = {type: "sourcecred/project", version: "0.3.0"};
export type ProjectJSON = Compatible<Project>;

View File

@ -18,11 +18,18 @@ describe("core/project", () => {
id: "foo/bar",
repoIds: [foobar],
discourseServer: null,
identities: [],
});
const p2: Project = deepFreeze({
id: "@foo",
repoIds: [foobar, foozod],
discourseServer: {serverUrl: "https://example.com", apiUsername: "credbot"},
identities: [
{
username: "example",
aliases: ["github/example"],
},
],
});
describe("to/fro JSON", () => {
it("round trip is identity", () => {

View File

@ -23,11 +23,13 @@ describe("core/project_io", () => {
id: "foo/bar",
repoIds: [foobar],
discourseServer: null,
identities: [],
});
const p2: Project = deepFreeze({
id: "@foo",
repoIds: [foobar, foozod],
discourseServer: {serverUrl: "https://example.com", apiUsername: "credbot"},
identities: [{username: "foo", aliases: ["github/foo", "discourse/foo"]}],
});
it("setupProjectDirectory results in a loadable project", async () => {

View File

@ -37,6 +37,7 @@ export async function specToProject(
id: spec,
repoIds: [stringToRepoId(spec)],
discourseServer: null,
identities: [],
};
return project;
} else if (spec.match(ownerSpecMatcher)) {
@ -46,6 +47,7 @@ export async function specToProject(
id: spec,
repoIds: org.repos,
discourseServer: null,
identities: [],
};
return project;
}

View File

@ -18,6 +18,7 @@ describe("plugins/github/specToProject", () => {
id: spec,
repoIds: [stringToRepoId(spec)],
discourseServer: null,
identities: [],
};
const actual = await specToProject(spec, "FAKE_TOKEN");
expect(expected).toEqual(actual);
@ -31,7 +32,12 @@ describe("plugins/github/specToProject", () => {
fetchGithubOrg.mockResolvedValueOnce(fakeOrg);
const actual = await specToProject(spec, token);
expect(fetchGithubOrg).toHaveBeenCalledWith(fakeOrg.name, token);
const expected: Project = {id: spec, repoIds: repos, discourseServer: null};
const expected: Project = {
id: spec,
repoIds: repos,
discourseServer: null,
identities: [],
};
expect(actual).toEqual(expected);
});
describe("fails for malformed spec strings", () => {