Skip to content

Commit

Permalink
Merge pull request #28 from yurenju/feat/e2e-server-2
Browse files Browse the repository at this point in the history
Feat/e2e server 2
  • Loading branch information
yurenju authored Sep 26, 2023
2 parents 8b21aae + f1ef7d4 commit c6bf344
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 53 deletions.
59 changes: 17 additions & 42 deletions apps/server/src/ethereum/ethereum.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,14 @@ import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { SiweMessage } from 'siwe';
import { setupMongoDb, setupTestApplication } from '../../tests/helper';
import {
generatePayload,
getDomain,
setupMongoDb,
setupTestApplication,
} from '../../tests/helper';
import { getRandomHexString } from '../utils';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getDomain(server: any): string {
server.listen();
const address = server.address();
return `127.0.0.1:${address.port}`;
}

function generateSiweMessage(domain: string, address: string, nonce: string) {
const rawMessage = new SiweMessage({
domain,
address,
statement: 'Sign in with Ethereum to the app.',
uri: `http://${domain}`,
version: '1',
chainId: 1,
nonce,
});
const message = rawMessage.prepareMessage() as `0x${string}`;

return message;
}

describe('EthereumModule', () => {
let app: INestApplication;
let mongod: MongoMemoryServer;
Expand Down Expand Up @@ -59,18 +41,12 @@ describe('EthereumModule', () => {

const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);
const message = generateSiweMessage(
const payload = await generatePayload(
id,
domain,
account.address,
account,
challengeRes.body.value
);
const signature = await account.signMessage({ message });
const payload = {
id,
account: account.address,
message,
signature,
};

await request(server)
.post('/api/auth/ethereum/login')
Expand All @@ -94,23 +70,22 @@ describe('EthereumModule', () => {

const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);
const message = generateSiweMessage(
const payload = await generatePayload(
id,
domain,
account.address,
account,
getRandomHexString()
);
const signature = await account.signMessage({ message });
const payload = {
id,
account: account.address,
message,
signature,
};

request(server)
.post('/api/auth/ethereum/login')
.send(payload)
.set('Authorization', `Bearer ${token}`)
.expect(401);
});

it('should fail to challenge without a JWT token', async () => {
const server = app.getHttpServer();
return request(server).post('/api/auth/ethereum/challenge').expect(401);
});
});
2 changes: 1 addition & 1 deletion apps/server/src/ethereum/nonce.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class NonceService {
.findOneAndDelete({ value: nonceValue })
.exec();

// this callback hander is for compatible of passport-siwe
// this callback is for compatible of passport-siwe
if (!nonce) {
const error = new NotFoundException('Nonce not found');
if (cb) {
Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/national/national.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Body,
ConflictException,
Controller,
Get,
Post,
Expand All @@ -11,6 +12,7 @@ import { NationalService } from './national.service';
import { RegisterNationalDto } from './register-national.dto';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { CreateUserDto } from '../user/create-user.dto';

@Controller('auth/national')
export class NationalController {
Expand All @@ -28,7 +30,8 @@ export class NationalController {
@Post('register')
async register(@Body() registerNationalDto: RegisterNationalDto) {
const nationalId = registerNationalDto.username;
const user = await this.usersService.findOrCreate(nationalId);
const createUserDto: CreateUserDto = { nationalId };
const user = await this.usersService.createUnique(createUserDto);
return this.nationalService.login(user);
}

Expand Down
26 changes: 26 additions & 0 deletions apps/server/src/national/national.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,30 @@ describe('NationalModule', () => {
expect(res.body).toHaveProperty('user');
});
});

it('should fail to login with incorrect username', async () => {
const server = app.getHttpServer();

await request(server)
.post('/api/auth/national/login')
.send({ username: 'non-exist', password: 'password' })
.expect(401);
});

it('should fail to register with existing username', async () => {
const server = app.getHttpServer();
await request(server)
.post('/api/auth/national/register')
.send({ username: 'username', password: 'password' });
return request(server)
.post('/api/auth/national/register')
.send({ username: 'username', password: 'password' })
.expect(409);
});

it('should fail to check without a JWT token', async () => {
return request(app.getHttpServer())
.get('/api/auth/national/check')
.expect(401);
});
});
8 changes: 1 addition & 7 deletions apps/server/src/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { User } from './user.schema';
import { UsersService } from './user.service';
import { CreateUserDto } from './create-user.dto';
import { JwtAuthGuard } from '../national/guards/jwt-auth.guard';

@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}

@Post()
create(@Body() createUserDto: CreateUserDto) {
this.usersService.create(createUserDto);
}

@UseGuards(JwtAuthGuard)
@Get()
findAll(): Promise<User[]> {
Expand Down
70 changes: 70 additions & 0 deletions apps/server/src/user/user.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { INestApplication } from '@nestjs/common';
import { Identity } from '@semaphore-protocol/identity';
import request from 'supertest';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import {
generatePayload,
getDomain,
setupMongoDb,
setupTestApplication,
} from '../../tests/helper';

describe('UsersModule', () => {
let app: INestApplication;
let mongod: MongoMemoryServer;

beforeEach(async () => {
mongod = await setupMongoDb();
app = await setupTestApplication(mongod.getUri());
});

afterEach(async () => {
await mongod.stop();
await app.close();
});

it('should get an array of semaphore commitments', async () => {
const server = app.getHttpServer();
const domain = getDomain(server);

const res = await request(server)
.post('/api/auth/national/register')
.send({ username: 'username', password: 'password' });

const { id, token } = res.body;

const challengeRes = await request(server)
.post('/api/auth/ethereum/challenge')
.set('Authorization', `Bearer ${token}`)
.expect(201);

const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);
const payload = await generatePayload(
id,
domain,
account,
challengeRes.body.value
);

const identity = new Identity();
const commitment = identity.commitment.toString();

await request(server)
.post('/api/auth/ethereum/login')
.send(payload)
.set('Authorization', `Bearer ${token}`);

await request(server)
.put('/api/auth/semaphore')
.send({ id, commitment })
.set('Authorization', `Bearer ${token}`);

await request(server)
.get('/api/users/commitments')
.expect((res) => {
expect(res.body).toEqual([commitment]);
});
});
});
14 changes: 12 additions & 2 deletions apps/server/src/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { ConflictException, Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from './user.schema';
import { CreateUserDto } from './create-user.dto';
Expand All @@ -8,7 +8,7 @@ import { Model } from 'mongoose';
export class UsersService {
constructor(@InjectModel(User.name) private userModel: Model<User>) {}

async create(createUserDto: CreateUserDto): Promise<User> {
async create(createUserDto: CreateUserDto): Promise<UserDocument> {
const createdUser = new this.userModel(createUserDto);
return createdUser.save();
}
Expand All @@ -35,6 +35,16 @@ export class UsersService {
);
}

async createUnique(createUserDto: CreateUserDto): Promise<UserDocument> {
const existingUser = await this.findOne(createUserDto.nationalId);

if (existingUser) {
throw new ConflictException('National ID already exists');
}

return this.create(createUserDto);
}

updateEthereumAccount(id: string, ethereumAccount: string) {
return this.userModel.findByIdAndUpdate(id, {
ethereumAccount,
Expand Down
46 changes: 46 additions & 0 deletions apps/server/tests/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Test, TestingModule } from '@nestjs/testing';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { UsersModule } from '../src/user/user.module';
import { AuthModule } from '../src/auth/auth.module';
import { SiweMessage } from 'siwe';
import { PrivateKeyAccount } from 'viem/accounts';

export function setupMongoDb(): Promise<MongoMemoryServer> {
return MongoMemoryServer.create();
Expand All @@ -21,3 +23,47 @@ export async function setupTestApplication(
await app.init();
return app;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getDomain(server: any): string {
server.listen();
const address = server.address();
return `127.0.0.1:${address.port}`;
}

export function generateSiweMessage(
domain: string,
address: string,
nonce: string
) {
const rawMessage = new SiweMessage({
domain,
address,
statement: 'Sign in with Ethereum to the app.',
uri: `http://${domain}`,
version: '1',
chainId: 1,
nonce,
});
const message = rawMessage.prepareMessage() as `0x${string}`;

return message;
}

export async function generatePayload(
id: string,
domain: string,
account: PrivateKeyAccount,
nonce: string
) {
const message = generateSiweMessage(domain, account.address, nonce);
const signature = await account.signMessage({ message });
const payload = {
id,
account: account.address,
message,
signature,
};

return payload;
}
1 change: 1 addition & 0 deletions apps/server/tsconfig.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
},
"include": [
"jest.config.ts",
"src/test.end.d.ts",
"tests/**.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
Expand Down

0 comments on commit c6bf344

Please sign in to comment.