diff --git a/src/plugins/identity/alias.js b/src/plugins/identity/alias.js index 75fb283..846d6ee 100644 --- a/src/plugins/identity/alias.js +++ b/src/plugins/identity/alias.js @@ -4,6 +4,10 @@ import {type NodeAddressT} from "../../core/graph"; import {githubOwnerPattern} from "../github/repoId"; import {loginAddress as githubAddress} from "../github/nodes"; 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. * @@ -49,6 +53,13 @@ export function resolveAlias( } 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: throw new Error(`Unknown type for alias: ${alias}`); } diff --git a/src/plugins/identity/alias.test.js b/src/plugins/identity/alias.test.js index dd624a5..8e37604 100644 --- a/src/plugins/identity/alias.test.js +++ b/src/plugins/identity/alias.test.js @@ -3,6 +3,7 @@ import {resolveAlias} from "./alias"; import {loginAddress as githubAddress} from "../github/nodes"; import {userAddress as discourseAddress} from "../discourse/address"; +import {identityAddress} from "./identity"; describe("src/plugins/identity/alias", () => { describe("resolveAlias", () => { @@ -77,6 +78,16 @@ describe("src/plugins/identity/alias", () => { const b = resolveAlias("discourse/@login", url); 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); + }); }); }); }); diff --git a/src/plugins/identity/identity.js b/src/plugins/identity/identity.js index 1f34331..f0226fa 100644 --- a/src/plugins/identity/identity.js +++ b/src/plugins/identity/identity.js @@ -3,6 +3,7 @@ import {NodeAddress, type Node} from "../../core/graph"; import {nodePrefix} from "./declaration"; import {type Alias} from "./alias"; +import type {NodeAddressT} from "../../core/graph"; /** * A Username is a locally (within-instance) unique identifier for a user of @@ -30,17 +31,31 @@ export type IdentitySpec = {| +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. */ export function identityNode(identity: Identity): Node { - const re = new RegExp(USERNAME_PATTERN); - const match = re.exec(identity.username); - if (match == null) { - throw new Error(`Invalid username: ${identity.username}`); - } - const username = match[1]; - const address = NodeAddress.append(nodePrefix, username); + const username = validateUsername(identity.username); + const address = identityAddress(username); const description = `@${username}`; return {address, timestampMs: null, description}; } + +export function identityAddress(username: Username): NodeAddressT { + return NodeAddress.append(nodePrefix, validateUsername(username)); +} diff --git a/src/plugins/identity/identity.test.js b/src/plugins/identity/identity.test.js index a66a31b..ed27254 100644 --- a/src/plugins/identity/identity.test.js +++ b/src/plugins/identity/identity.test.js @@ -1,26 +1,51 @@ // @flow import {NodeAddress} from "../../core/graph"; -import {identityNode} from "./identity"; +import {identityAddress, identityNode, validateUsername} from "./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", () => { const identity = {username: "foo", aliases: ["github/foo"]}; const n = identityNode(identity); expect(n.address).toEqual( NodeAddress.fromParts(["sourcecred", "identity", "foo"]) ); + expect(identityAddress(identity.username)).toEqual(n.address); expect(n.timestampMs).toEqual(null); expect(n.description).toEqual("@foo"); }); it("errors for an empty username", () => { 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", () => { 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", () => { const identity = {username: "@foo", aliases: ["github/foo"]}; @@ -28,6 +53,7 @@ describe("src/plugins/identity/identity", () => { expect(n.address).toEqual( NodeAddress.fromParts(["sourcecred", "identity", "foo"]) ); + expect(identityAddress(identity.username)).toEqual(n.address); expect(n.timestampMs).toEqual(null); expect(n.description).toEqual("@foo"); });