Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delegatee registration form #51

Merged
merged 24 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ jobs:
needs: [build]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: actions/cache@v4
with:
path: |
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ yarn-error.log*
.vercel

# typescript
*.tsbuildinfo
*.tsbuildinfo
.env*.local
7 changes: 2 additions & 5 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn lint-staged
yarn prettier
yarn lint-staged --quiet
yarn prettier
893 changes: 0 additions & 893 deletions .yarn/releases/yarn-4.1.1.cjs

This file was deleted.

925 changes: 925 additions & 0 deletions .yarn/releases/yarn-4.5.0.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
spec: "https://mskelton.dev/yarn-outdated/v3"

yarnPath: .yarn/releases/yarn-4.1.1.cjs
yarnPath: .yarn/releases/yarn-4.5.0.cjs
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"micromark": "^4.0.0",
"micromark-extension-gfm-table": "^2.0.0",
"next": "14.2.10",
"octokit": "^4.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-minimal-pie-chart": "^8.4.0",
Expand All @@ -44,7 +45,6 @@
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.28.6",
"@types/dompurify": "^3",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.30",
"@types/react": "^18.2.73",
"@types/react-dom": "^18.2.22",
Expand All @@ -58,7 +58,6 @@
"eslint-config-next": "^14.1.4",
"eslint-config-prettier": "^9.1.0",
"husky": "^9.0.11",
"jest": "^29.7.0",
"lint-staged": "^15.2.2",
"postcss": "^8.4.38",
"prettier": "^3.3.3",
Expand All @@ -68,7 +67,8 @@
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"tsx": "^4.7.1",
"typescript": "^5.4.3"
"typescript": "^5.4.3",
"vitest": "^2.1.1"
},
"resolutions": {
"bignumber.js": "9.1.2",
Expand All @@ -79,13 +79,14 @@
"prepare": "husky",
"clean": "rm -rf dist cache .next",
"dev": "next dev",
"build": "yarn build:proposals && yarn build:app",
"build": "yarn build:proposals && yarn build:delegatees-json && yarn build:app",
"build:delegatees-json": "tsx ./src/scripts/combineDelegateesMetadata.ts",
"build:proposals": "tsx ./src/scripts/collectProposalMetadata.ts",
"build:app": "next build",
"typecheck": "tsc",
"lint": "next lint",
"start": "next start",
"test": "jest",
"test": "vitest",
"prettier": "prettier --write ./src",
"checks": "yarn typecheck && yarn lint && yarn test && yarn prettier && yarn build:app"
},
Expand Down
152 changes: 152 additions & 0 deletions src/app/delegate/api/register/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* @jest-environment node
aaronmgdr marked this conversation as resolved.
Show resolved Hide resolved
*/
import fs from 'fs';
import path from 'path';
import * as utils from 'src/features/delegation/utils';
import { getValidRequest } from 'src/test/delegatee-registration-utils';
import { expect, it, vi } from 'vitest';
import { POST } from './route';

const getRequest = (formData: FormData): Request => {
return {
formData: () => formData,
} as any as Request;
};

const getValidFormData = async () => {
const request = await getValidRequest();
const data = new FormData();
const imageBuffer = fs.readFileSync(
path.join(__dirname, '../../../../../public/logos/validators/clabs.jpg'),
);

data.append('image', new Blob([imageBuffer], { type: 'image/jpeg' }), 'clabs.jpg');
data.append('name', request.name);
data.append('interests', request.interests);
data.append('description', request.description);
data.append('signature', request.signature as HexString);
data.append('twitterUrl', request.twitterUrl as string);
data.append('websiteUrl', request.websiteUrl as string);
data.append('verificationUrl', request.verificationUrl as string);
data.append('address', request.address);

return data;
};

it('successfuly calls PR creation', async () => {
const createDelegationPRMock = vi.spyOn(utils, 'createDelegationPR');
const isAddressAnAccountMock = vi.spyOn(utils, 'isAddressAnAccount');

createDelegationPRMock.mockResolvedValueOnce('http://example.com/pull-request');
isAddressAnAccountMock.mockResolvedValueOnce(true);

const response = await POST(getRequest(await getValidFormData()));

expect(response.status).toBe(200);

const body = await response.json();

expect(body).toMatchInlineSnapshot(`
{
"pullRequestUrl": "http://example.com/pull-request",
"status": "success",
}
`);
expect(isAddressAnAccountMock).toHaveBeenCalledTimes(1);
expect(createDelegationPRMock).toHaveBeenCalledTimes(1);
expect(createDelegationPRMock.mock.lastCall).toMatchInlineSnapshot(`
[
{
"address": "0x6A5DD51Da29914e8659b9CC354B414f30c7692c4",
"description": "Delegatee description",
"image": File {
Symbol(kHandle): Blob {},
Symbol(kLength): 3801,
Symbol(kType): "image/jpeg",
},
"interests": "blockchain, NFTs",
"name": "Delegatee name",
"signature": "0x52a3c23ef6c6817691872b77615ef30927453d641acd8c607de458d39e581bcd5411f723102640897af151644086abf4f3a9baf216d684d784194aef2c6730be1c",
"twitterUrl": "https://example.com/x",
"verificationUrl": "https://example.com/verification",
"websiteUrl": "https://example.com",
},
]
`);
});

it('handles validation errors', async () => {
const createDelegationPRMock = vi.spyOn(utils, 'createDelegationPR');
const isAddressAnAccountMock = vi.spyOn(utils, 'isAddressAnAccount');

isAddressAnAccountMock.mockResolvedValueOnce(false);

const response = await POST(getRequest(await getValidFormData()));

expect(response.status).toBe(400);

const body = await response.json();

expect(body).toMatchInlineSnapshot(`
{
"errors": {
"address": "Address is not an account",
},
"message": "Invalid delegatee registration request",
"status": "error",
}
`);
expect(isAddressAnAccountMock).toHaveBeenCalledTimes(1);
expect(createDelegationPRMock).not.toHaveBeenCalled();
});

it('handles PR creation error', async () => {
const createDelegationPRMock = vi.spyOn(utils, 'createDelegationPR');
const isAddressAnAccountMock = vi.spyOn(utils, 'isAddressAnAccount');

createDelegationPRMock.mockRejectedValueOnce(new Error('Mock PR creation error'));

isAddressAnAccountMock.mockResolvedValueOnce(true);

const response = await POST(getRequest(await getValidFormData()));

expect(response.status).toBe(500);

const body = await response.json();

expect(body).toMatchInlineSnapshot(`
{
"message": "Error creating PR",
"status": "error",
}
`);
expect(isAddressAnAccountMock).toHaveBeenCalledTimes(1);
expect(createDelegationPRMock).toHaveBeenCalledTimes(1);
});

it('handles generic error', async () => {
const createDelegationPRMock = vi.spyOn(utils, 'createDelegationPR');
const isAddressAnAccountMock = vi.spyOn(utils, 'isAddressAnAccount');

isAddressAnAccountMock.mockResolvedValueOnce(true);

const response = await POST({
formData: () => {
throw new Error('Mock error');
},
} as any as Request);

expect(response.status).toBe(400);

const body = await response.json();

expect(body).toMatchInlineSnapshot(`
{
"message": "Error parsing request",
"status": "error",
}
`);
expect(isAddressAnAccountMock).not.toHaveBeenCalled();
expect(createDelegationPRMock).not.toHaveBeenCalled();
});
102 changes: 102 additions & 0 deletions src/app/delegate/api/register/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { NextResponse } from 'next/server';
import {
RegisterDelegateResponse,
RegisterDelegateResponseStatus,
} from 'src/features/delegation/types';
import { createDelegationPR } from 'src/features/delegation/utils';
import { validateRegistrationRequest } from 'src/features/delegation/validateRegistrationRequest';
import { logger } from 'src/utils/logger';
import { Hex } from 'viem';

type RegisterDelegateRequest = {
name: string;
address: Address;
image: File;
websiteUrl: string;
twitterUrl: string;
verificationUrl: string;
interests: string;
description: string;
signature: Hex;
};

export const dynamic = 'force-dynamic';

export async function POST(httpRequest: Request) {
let registrationRequest: RegisterDelegateRequest;

try {
const data = await httpRequest.formData();

const address = data.get('address') as Address;
const signature = data.get('signature') as Hex;
const name = data.get('name') as string;
const websiteUrl = data.get('websiteUrl') as string;
const twitterUrl = data.get('twitterUrl') as string;
const verificationUrl = data.get('verificationUrl') as string;
const interests = data.get('interests') as string;
const description = data.get('description') as string;
const image = data.get('image') as File;

registrationRequest = {
address,
image,
name,
websiteUrl,
twitterUrl,
verificationUrl,
interests,
description,
signature,
};

const validationResult = await validateRegistrationRequest(registrationRequest, true);

if (Object.keys(validationResult).length) {
return wrapResponseInJson(
{
status: RegisterDelegateResponseStatus.Error,
message: 'Invalid delegatee registration request',
errors: validationResult,
},
400,
shazarre marked this conversation as resolved.
Show resolved Hide resolved
);
}
} catch (error) {
return wrapResponseInJson(
{
status: RegisterDelegateResponseStatus.Error,
message: 'Error parsing request',
},
400,
);
}

try {
const url = await createDelegationPR(registrationRequest);

return wrapResponseInJson({
status: RegisterDelegateResponseStatus.Success,
pullRequestUrl: url,
});
} catch (err) {
logger.error('Error creating PR: ', err);

return wrapResponseInJson(
{
status: RegisterDelegateResponseStatus.Error,
message: 'Error creating PR',
},
500,
);
}
}

function wrapResponseInJson(response: RegisterDelegateResponse, status: number = 200) {
return NextResponse.json(response, {
headers: {
'Content-Type': 'application/json',
},
status,
});
}
12 changes: 5 additions & 7 deletions src/app/delegate/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
'use client';

import Link from 'next/link';
import { Fade } from 'src/components/animation/Fade';
import { FullWidthSpinner } from 'src/components/animation/Spinner';
import { A_Blank } from 'src/components/buttons/A_Blank';
import { CtaCard } from 'src/components/layout/CtaCard';
import { Section } from 'src/components/layout/Section';
import { H1 } from 'src/components/text/headers';
import { links } from 'src/config/links';
import { DelegateesTable } from 'src/features/delegation/components/DelegateesTable';
import { useDelegatees } from 'src/features/delegation/hooks/useDelegatees';

Expand Down Expand Up @@ -42,13 +41,12 @@ function RegisterCtaCard() {
<div className="space-y-2">
<h3 className="font-serif text-xl sm:text-2xl">Passionate about Celo governance?</h3>
<p className="text-sm sm:text-base">
If you would like to be included in this list, open a pull-request to add your information
on Github.
If you would like to be included in this list, fill a form by clicking the button.
aaronmgdr marked this conversation as resolved.
Show resolved Hide resolved
</p>
</div>
<A_Blank href={links.delegate} className="btn btn-primary rounded-full border-taupe-300">
Register as a delegate
</A_Blank>
<Link href="/delegate/register" className="btn btn-primary rounded-full border-taupe-300">
Register as a delegatee
</Link>
</CtaCard>
);
}
Loading