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 {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}`);
}

View File

@ -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);
});
});
});
});

View File

@ -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));
}

View File

@ -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");
});