Skip to content
This repository has been archived by the owner on Aug 1, 2022. It is now read-only.

Commit

Permalink
feat: register member endpoint integration (#446)
Browse files Browse the repository at this point in the history
* User register member endpoint
* Show actual org members
* Correct member registration message
* Add register user control endpoint
  • Loading branch information
Merle Breitkreuz committed Jun 4, 2020
1 parent 6c557b6 commit 80b4a6e
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 95 deletions.
84 changes: 45 additions & 39 deletions cypress/integration/org_member_addition.spec.js
Expand Up @@ -5,6 +5,7 @@ before(() => {
cy.createIdentity("coolname");
cy.registerUser("coolname");
cy.registerOrg("coolorg");
cy.registerAlternativeUser("user2");
});

context("add member to org", () => {
Expand All @@ -14,6 +15,32 @@ context("add member to org", () => {
cy.pick("org-screen", "add-member-button").click();
});

context("validations", () => {
it("prevents the user from adding an invalid user", () => {
// no empty input
cy.pick("input").type("aname");
cy.pick("input").clear();
cy.pick("add-member-modal").contains("Member handle is required");
cy.pick("submit-button").should("be.disabled");

// no non-existing users
cy.pick("input").type("aname");
cy.pick("add-member-modal").contains("Cannot find this user");
cy.pick("submit-button").should("be.disabled");
});
});

context("transaction confirmation", () => {
it("shows correct transaction details", () => {
cy.pick("input").type("user2");
cy.pick("submit-button").click();

// check the transaction details before submition
cy.pick("message").contains("Org member registration");
cy.pick("subject").contains("user2");
});
});

context("navigation", () => {
it("can be closed by pressing cancel", () => {
cy.pick("add-member-modal").contains("Register a member");
Expand All @@ -29,7 +56,7 @@ context("add member to org", () => {

it("can be traversed with navigation buttons", () => {
// form -> tx confirmation
cy.pick("input").type("coolname");
cy.pick("input").type("user2");
cy.pick("submit-button").click();
cy.pick("summary").should("exist");

Expand All @@ -44,47 +71,26 @@ context("add member to org", () => {
cy.pick("org-screen").should("exist");
});
});
});

context("validations", () => {
it("prevents the user from adding an invalid user", () => {
// no empty input
cy.pick("input").type("aname");
cy.pick("input").clear();
cy.pick("add-member-modal").contains("Member handle is required");
cy.pick("submit-button").should("be.disabled");
context("after submitting the transaction", () => {
it("shows correct transaction details", () => {
// Register a new member
cy.visit("public/index.html");
cy.pick("sidebar", "org-coolorg").click();

// no non-existing users
cy.pick("input").type("aname");
cy.pick("add-member-modal").contains("Cannot find this user");
cy.pick("submit-button").should("be.disabled");
});
// pick most recent transaction to check the transaction details
cy.pick("transaction-center").click();
cy.pick("transaction-center", "transaction-item").first().click();
cy.pick("summary", "message").contains("Org member registration");
cy.pick("summary", "subject").contains("user2");
});

context("transaction", () => {
it("shows correct transaction details for confirmation", () => {
cy.pick("input").type("coolname");
cy.pick("submit-button").click();

cy.pick("message").contains("Org member registration");
cy.pick("subject").contains("coolname");
});

// TODO(sos): add actual transaction details check once we can make this tx
it.skip("submits correct transaction details to proxy", () => {
cy.pick("input").type("coolname");
cy.pick("submit-button").click();
cy.pick("submit-button").click();

cy.pick("transaction-center").click();

// pick most recent transaction
cy.pick("transaction-center", "transaction-item").last().click();
cy.pick("summary", "message").contains("Org member registration");
cy.pick("summary", "subject").contains("coolname");
cy.pick("summary", "subject-avatar", "emoji").should(
"have.class",
"circle"
);
});
it("shows both users in the list", () => {
cy.visit("public/index.html");
cy.pick("sidebar", "org-coolorg").click();
cy.pick("horizontal-menu", "Members").click();
cy.pick("member-list").contains("coolname");
cy.pick("member-list").contains("user2");
});
});
14 changes: 14 additions & 0 deletions cypress/support/commands.js
Expand Up @@ -79,6 +79,20 @@ Cypress.Commands.add(
})
);

Cypress.Commands.add(
"registerAlternativeUser",
async (handle = "anotherUser") =>
await fetch("http://localhost:8080/v1/control/register-user", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
handle,
}),
})
);

Cypress.Commands.add(
"createIdentity",
async (
Expand Down
50 changes: 45 additions & 5 deletions proxy/src/http/control.rs
Expand Up @@ -29,8 +29,9 @@ pub fn routes<R: registry::Client>(
.and(
create_project_filter(Arc::clone(&librad_paths))
.or(nuke_coco_filter(librad_paths))
.or(nuke_registry_filter(registry))
.or(nuke_session_filter(store)),
.or(nuke_registry_filter(Arc::clone(&registry)))
.or(nuke_session_filter(store))
.or(register_user_filter(registry)),
)
}

Expand All @@ -43,11 +44,12 @@ fn filters<R: registry::Client>(
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
create_project_filter(Arc::clone(&librad_paths))
.or(nuke_coco_filter(librad_paths))
.or(nuke_registry_filter(registry))
.or(nuke_registry_filter(Arc::clone(&registry)))
.or(nuke_session_filter(store))
.or(register_user_filter(registry))
}

/// POST /nuke/create-project
/// POST /create-project
fn create_project_filter(
librad_paths: Arc<RwLock<paths::Paths>>,
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
Expand All @@ -57,6 +59,16 @@ fn create_project_filter(
.and_then(handler::create_project)
}

/// POST /register-user
fn register_user_filter<R: registry::Client>(
registry: http::Shared<R>,
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
path!("register-user")
.and(http::with_shared(registry))
.and(warp::body::json())
.and_then(handler::register_user)
}

/// GET /nuke/coco
fn nuke_coco_filter(
librad_paths: Arc<RwLock<paths::Paths>>,
Expand Down Expand Up @@ -88,6 +100,8 @@ fn nuke_session_filter(
mod handler {
use kv::Store;
use librad::paths::Paths;
use radicle_registry_client::Balance;
use std::convert::TryFrom;
use std::sync::Arc;
use tokio::sync::RwLock;
use warp::http::StatusCode;
Expand Down Expand Up @@ -130,6 +144,24 @@ mod handler {
))
}

/// Register a user with another key
pub async fn register_user<R: registry::Client>(
registry: http::Shared<R>,
input: super::RegisterInput,
) -> Result<impl Reply, Rejection> {
let fake_pair =
radicle_registry_client::ed25519::Pair::from_legacy_string(&input.handle, None);
let fake_fee: Balance = 100;

let handle = registry::Id::try_from(input.handle)?;
let reg = registry.write().await;
reg.register_user(&fake_pair, handle.clone(), None, fake_fee)
.await
.expect("unable to register user");

Ok(reply::json(&true))
}

/// Reset the coco state by creating a new temporary directory for the librad paths.
pub async fn nuke_coco(librad_paths: Arc<RwLock<Paths>>) -> Result<impl Reply, Rejection> {
let dir = tempfile::tempdir().expect("tmp dir creation failed");
Expand Down Expand Up @@ -165,10 +197,18 @@ mod handler {
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateInput {
/// Name of the proejct.
/// Name of the project.
name: String,
/// Long form outline.
description: String,
/// Configured default branch.
default_branch: String,
}

/// Input for user registration.
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RegisterInput {
/// Handle of the user.
handle: String,
}
5 changes: 4 additions & 1 deletion proxy/src/registry.rs
Expand Up @@ -501,7 +501,10 @@ impl Client for Registry {
let tx = Transaction::confirmed(
Hash(applied.tx_hash),
block.number,
Message::OrgRegistration { id: org_id.clone() },
Message::MemberRegistration {
org_id: org_id.clone(),
handle: user_id,
},
);

Ok(tx)
Expand Down
8 changes: 8 additions & 0 deletions proxy/src/registry/transaction.rs
Expand Up @@ -109,6 +109,14 @@ pub enum Message {
/// Identity id originated from librad.
id: Option<String>,
},

/// Issue a member registration for a given handle under a given org.
MemberRegistration {
/// Globally unique user handle.
handle: registry::Id,
/// The Org in which to register the member.
org_id: registry::Id,
},
}

/// Possible states a [`Transaction`] can have. Useful to reason about the lifecycle and
Expand Down
2 changes: 1 addition & 1 deletion ui/DesignSystem/Component/HorizontalMenu.svelte
Expand Up @@ -33,7 +33,7 @@
}
</style>

<nav data-cy="project-topbar-menu">
<nav data-cy="horizontal-menu">
<ul class="menu-list">
{#each items as item, i}
<li class="menu-list-item">
Expand Down
2 changes: 1 addition & 1 deletion ui/DesignSystem/Component/HorizontalMenu/MenuItem.svelte
Expand Up @@ -42,7 +42,7 @@

<!-- svelte-spa-router link action is not reactive and breaks if the href
changes dynamically, this is why we have to spell out the href manually -->
<a href={`#${href}`}>
<a href={`#${href}`} data-cy={title}>
{#if active}
<div class="icon">
<svelte:component this={icon} style="fill: var(--color-secondary)" />
Expand Down
2 changes: 2 additions & 0 deletions ui/Screen/Org/MemberRegistration.svelte
Expand Up @@ -3,6 +3,7 @@
import {
RegistrationFlowState,
registerMember,
registerMemberTransaction,
memberHandleValidationStore,
} from "../../src/org.ts";
Expand Down Expand Up @@ -38,6 +39,7 @@
}
break;
case RegistrationFlowState.Confirmation:
registerMember(orgId, userHandle);
pop();
}
};
Expand Down
47 changes: 27 additions & 20 deletions ui/Screen/Org/Members.svelte
@@ -1,10 +1,11 @@
<script>
import { mockMemberList } from "../../src/org.ts";
import { org as store } from "../../src/org.ts";
import { Icon, Text, Title } from "../../DesignSystem/Primitive";
import {
AdditionalActionsDropdown,
List,
Remote,
} from "../../DesignSystem/Component";
// TODO(sos): replace console.log's with actual navigation
Expand Down Expand Up @@ -54,24 +55,30 @@
}
</style>

<List items={mockMemberList} let:item={member} on:select={select}>
<div class="member">
<div class="info">
<Title>{member.handle}</Title>
<Icon.Badge style="margin-left: 6px; fill: var(--color-primary);" />
</div>
<Remote {store} let:data={org}>
<List
items={org.members}
let:item={member}
on:select={select}
dataCy="member-list">
<div class="member">
<div class="info">
<Title>{member.handle}</Title>
<Icon.Badge style="margin-left: 6px; fill: var(--color-primary);" />
</div>

<div class="membership-details">
{#if member.pending}
<div class="pending">
<Text
variant="tiny"
style="color: var(--color-caution); padding: 8px;">
Pending
</Text>
</div>
{/if}
<AdditionalActionsDropdown menuItems={menuItems(member)} />
<div class="membership-details">
{#if member.pending}
<div class="pending">
<Text
variant="tiny"
style="color: var(--color-caution); padding: 8px;">
Pending
</Text>
</div>
{/if}
<AdditionalActionsDropdown menuItems={menuItems(member)} />
</div>
</div>
</div>
</List>
</List>
</Remote>
15 changes: 8 additions & 7 deletions ui/src/__mocks__/api.ts
Expand Up @@ -3,20 +3,21 @@ import { User } from "../user"

type MockedResponse = Org | User | null

const radicleMock = {
// just to give an idea of how we'd stub the api with other endpoints
const userMock: User = {
handle: "rafalca"
}

const radicleMock: Org = {
id: "radicle",
shareableEntityIdentifier: "radicle@123abcd.git",
avatarFallback: {
background: {
r: 255, g: 67, b: 34
},
emoji: "🔥"
}
}

// just to give an idea of how we'd stub the api with other endpoints
const userMock = {
handle: "rafalca"
},
members: [userMock]
}

export const get = async (endpoint: string): Promise<MockedResponse> => {
Expand Down
3 changes: 2 additions & 1 deletion ui/src/org.test.ts
Expand Up @@ -16,7 +16,8 @@ describe("fetching an org", () => {
r: 255, g: 67, b: 34
},
emoji: "🔥"
}
},
members: [{handle: "rafalca"}]
})
})
})
Expand Down

0 comments on commit 80b4a6e

Please sign in to comment.