resolveAlias: add support for sourcecred identity (#1752)

This commit modifies the resolveAlias function in the identity plugin's
alias module so that it now allows you to convert sourcecred identity
aliases (e.g. "sourcecred/user") back into NodeAddresses. This will be
necessary so that we can convert our ad-hoc distributions and transfers
(which use aliases, including SourceCred identity aliases) into the more
robust formats for the productionized grain ledger, which use full node
addresses.

I did a slight refactor on the identity module to expose a method for
constructing the address without the rest of the node.

Test plan: `yarn test`
This commit is contained in:
Dandelion Mané 2020-04-26 13:41:58 -07:00 committed by GitHub
parent 830c83bedf
commit 203b48066c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 74 additions and 11 deletions

View File

@ -4,6 +4,10 @@ import {type NodeAddressT} from "../../core/graph";
import {githubOwnerPattern} from "../github/repoId"; import {githubOwnerPattern} from "../github/repoId";
import {loginAddress as githubAddress} from "../github/nodes"; import {loginAddress as githubAddress} from "../github/nodes";
import {userAddress as discourseAddress} from "../discourse/address"; import {userAddress as discourseAddress} from "../discourse/address";
import {
identityAddress,
USERNAME_PATTERN as _VALID_IDENTITY_PATTERN,
} from "./identity";
/** An Alias is a string specification of an identity within another plugin. /** An Alias is a string specification of an identity within another plugin.
* *
@ -49,6 +53,13 @@ export function resolveAlias(
} }
return discourseAddress(discourseUrl, match[1]); return discourseAddress(discourseUrl, match[1]);
} }
case "sourcecred": {
const match = name.match(_VALID_IDENTITY_PATTERN);
if (!match) {
throw new Error(`Invalid SourceCred identity: ${name}`);
}
return identityAddress(match[1]);
}
default: default:
throw new Error(`Unknown type for alias: ${alias}`); throw new Error(`Unknown type for alias: ${alias}`);
} }

View File

@ -3,6 +3,7 @@
import {resolveAlias} from "./alias"; import {resolveAlias} from "./alias";
import {loginAddress as githubAddress} from "../github/nodes"; import {loginAddress as githubAddress} from "../github/nodes";
import {userAddress as discourseAddress} from "../discourse/address"; import {userAddress as discourseAddress} from "../discourse/address";
import {identityAddress} from "./identity";
describe("src/plugins/identity/alias", () => { describe("src/plugins/identity/alias", () => {
describe("resolveAlias", () => { describe("resolveAlias", () => {
@ -77,6 +78,16 @@ describe("src/plugins/identity/alias", () => {
const b = resolveAlias("discourse/@login", url); const b = resolveAlias("discourse/@login", url);
expect(a).toEqual(b); expect(a).toEqual(b);
}); });
it("a sourcecred identity", () => {
const actual = resolveAlias("sourcecred/example", null);
const expected = identityAddress("example");
expect(actual).toEqual(expected);
});
it("a sourcecred identity with prefixed @", () => {
const a = resolveAlias("sourcecred/example", null);
const b = resolveAlias("sourcecred/@example", null);
expect(a).toEqual(b);
});
}); });
}); });
}); });

View File

@ -3,6 +3,7 @@
import {NodeAddress, type Node} from "../../core/graph"; import {NodeAddress, type Node} from "../../core/graph";
import {nodePrefix} from "./declaration"; import {nodePrefix} from "./declaration";
import {type Alias} from "./alias"; import {type Alias} from "./alias";
import type {NodeAddressT} from "../../core/graph";
/** /**
* A Username is a locally (within-instance) unique identifier for a user of * A Username is a locally (within-instance) unique identifier for a user of
@ -30,17 +31,31 @@ export type IdentitySpec = {|
+discourseServerUrl: string | null, +discourseServerUrl: string | null,
|}; |};
/**
* Internal method for validating a username.
*
* Returns the username with any leading @ symbol stripped.
* Throws an error if the username is invalid.
*/
export function validateUsername(username: string): Username {
const re = new RegExp(USERNAME_PATTERN);
const match = re.exec(username);
if (match == null) {
throw new Error(`invalid username: ${username}`);
}
return match[1];
}
/** /**
* Create a new node representing an identity. * Create a new node representing an identity.
*/ */
export function identityNode(identity: Identity): Node { export function identityNode(identity: Identity): Node {
const re = new RegExp(USERNAME_PATTERN); const username = validateUsername(identity.username);
const match = re.exec(identity.username); const address = identityAddress(username);
if (match == null) {
throw new Error(`Invalid username: ${identity.username}`);
}
const username = match[1];
const address = NodeAddress.append(nodePrefix, username);
const description = `@${username}`; const description = `@${username}`;
return {address, timestampMs: null, description}; return {address, timestampMs: null, description};
} }
export function identityAddress(username: Username): NodeAddressT {
return NodeAddress.append(nodePrefix, validateUsername(username));
}

View File

@ -1,26 +1,51 @@
// @flow // @flow
import {NodeAddress} from "../../core/graph"; import {NodeAddress} from "../../core/graph";
import {identityNode} from "./identity"; import {identityAddress, identityNode, validateUsername} from "./identity";
describe("src/plugins/identity/identity", () => { describe("src/plugins/identity/identity", () => {
describe("identityNode", () => { describe("validateUsername", () => {
it("accepts good inputs", () => {
const good = ["foo", "123", "foo-123", "foo_bar-123", "_S-A-M_"];
const usernames = good.map(validateUsername);
expect(good).toEqual(usernames);
});
it("strips leading @-signs", () => {
const good = ["foo", "123", "foo-123", "foo_bar-123", "_S-A-M_"];
const usernames = good.map((g) => validateUsername("@" + g));
expect(good).toEqual(usernames);
});
it("rejects bad inputs", () => {
const bad = ["", "@", "@foo@", "foo$bar"];
for (const b of bad) {
expect(() => validateUsername(b)).toThrow("invalid username");
}
});
});
describe("identityNode & identityAddress", () => {
it("works as expected for valid identity", () => { it("works as expected for valid identity", () => {
const identity = {username: "foo", aliases: ["github/foo"]}; const identity = {username: "foo", aliases: ["github/foo"]};
const n = identityNode(identity); const n = identityNode(identity);
expect(n.address).toEqual( expect(n.address).toEqual(
NodeAddress.fromParts(["sourcecred", "identity", "foo"]) NodeAddress.fromParts(["sourcecred", "identity", "foo"])
); );
expect(identityAddress(identity.username)).toEqual(n.address);
expect(n.timestampMs).toEqual(null); expect(n.timestampMs).toEqual(null);
expect(n.description).toEqual("@foo"); expect(n.description).toEqual("@foo");
}); });
it("errors for an empty username", () => { it("errors for an empty username", () => {
const identity = {username: "", aliases: ["github/foo"]}; const identity = {username: "", aliases: ["github/foo"]};
expect(() => identityNode(identity)).toThrowError("Invalid username"); expect(() => identityNode(identity)).toThrowError("invalid username");
expect(() => identityAddress(identity.username)).toThrowError(
"invalid username"
);
}); });
it("errors for a bad username", () => { it("errors for a bad username", () => {
const identity = {username: "$foo$bar", aliases: ["github/foo"]}; const identity = {username: "$foo$bar", aliases: ["github/foo"]};
expect(() => identityNode(identity)).toThrowError("Invalid username"); expect(() => identityNode(identity)).toThrowError("invalid username");
expect(() => identityAddress(identity.username)).toThrowError(
"invalid username"
);
}); });
it("strips redundant leading @ from the description and address", () => { it("strips redundant leading @ from the description and address", () => {
const identity = {username: "@foo", aliases: ["github/foo"]}; const identity = {username: "@foo", aliases: ["github/foo"]};
@ -28,6 +53,7 @@ describe("src/plugins/identity/identity", () => {
expect(n.address).toEqual( expect(n.address).toEqual(
NodeAddress.fromParts(["sourcecred", "identity", "foo"]) NodeAddress.fromParts(["sourcecred", "identity", "foo"])
); );
expect(identityAddress(identity.username)).toEqual(n.address);
expect(n.timestampMs).toEqual(null); expect(n.timestampMs).toEqual(null);
expect(n.description).toEqual("@foo"); expect(n.description).toEqual("@foo");
}); });