Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { EntityManager } from "typeorm";
import { RoomUserModel } from "../../../../../model/room/RoomUser";
import { RoomModel } from "../../../../../model/room/Room";
import { RoomStatus } from "../../../../../model/room/Constants";
import { dataSource } from "../../../../../thirdPartyService/TypeORMService";

export const alreadyJoinedRoomCount = async (userUUID: string): Promise<number> => {
return await dataSource
export const alreadyJoinedRoomCount = async (
userUUID: string,
t?: EntityManager,
): Promise<number> => {
return await (t || dataSource)
.createQueryBuilder(RoomUserModel, "ru")
.innerJoin(RoomModel, "r", "ru.room_uuid = r.room_uuid")
.andWhere("ru.user_uuid = :userUUID", {
Expand Down
2 changes: 2 additions & 0 deletions src/v2/__tests__/helpers/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CreateSecretsInfos } from "./oauth-secret";
import { CreateRoomJoin } from "./room-join";
import { CreateRoom } from "./room";
import { CreateUserPhone } from "./user-phone";
import { CreateUserWeChat } from "./user-wechat";

export const testService = (t: EntityManager) => {
return {
Expand All @@ -24,5 +25,6 @@ export const testService = (t: EntityManager) => {
createSecretsInfos: new CreateSecretsInfos(t),
createUser: new CreateUser(t),
createUserPhone: new CreateUserPhone(t),
createUserWeChat: new CreateUserWeChat(t),
};
};
2 changes: 1 addition & 1 deletion src/v2/__tests__/helpers/db/user-phone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class CreateUserPhone {
}
}

function randomPhoneNumber() {
export function randomPhoneNumber() {
const prefixArray = ["130", "131", "132", "133", "135", "137", "138", "170", "187", "189"];
const i = parseInt(String(10 * Math.random()));
let prefix = prefixArray[i];
Expand Down
38 changes: 38 additions & 0 deletions src/v2/__tests__/helpers/db/user-wechat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { EntityManager } from "typeorm";
import { v4 } from "uuid";
import { userWeChatDAO } from "../../../dao";

export class CreateUserWeChat {
public constructor(private readonly t: EntityManager) {}

public async full(info: {
userUUID: string;
userName: string;
unionUUID: string;
openUUID: string;
}) {
await userWeChatDAO.insert(this.t, {
user_uuid: info.userUUID,
user_name: info.userName,
union_uuid: info.unionUUID,
open_uuid: info.openUUID,
});
return info;
}

public async quick(info: {
userUUID?: string;
userName?: string;
unionUUID?: string;
openUUID?: string;
}) {
const fullInfo = {
userUUID: info.userUUID || v4(),
userName: info.userName || v4().slice(6),
unionUUID: info.unionUUID || v4(),
openUUID: info.openUUID || v4(),
};
await this.full(fullInfo);
return fullInfo;
}
}
32 changes: 32 additions & 0 deletions src/v2/controllers/user/rebind-phone/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Type } from "@sinclair/typebox";
import { FastifyReply } from "fastify";
import { LoginPlatform } from "../../../../constants/Project";
import { FastifyRequestTypebox, Response } from "../../../../types/Server";
import { UserRebindPhoneService, UserRebindReturn } from "../../../services/user/rebind-phone";
import { successJSON } from "../../internal/utils/response-json";

export const userRebindPhoneSchema = {
body: Type.Object(
{
phone: Type.String(),
code: Type.Integer(),
},
{
additionalProperties: false,
},
),
};

export const userRebindPhone = async (
req: FastifyRequestTypebox<typeof userRebindPhoneSchema>,
reply: FastifyReply,
): Promise<Response<UserRebindReturn>> => {
const service = new UserRebindPhoneService(req.ids, req.DBTransaction, req.userUUID);

const jwtSign = (userUUID: string): Promise<string> =>
reply.jwtSign({ userUUID, loginSource: LoginPlatform.Phone });

const result = await service.rebind(req.body.phone, req.body.code, jwtSign);

return successJSON(result);
};
25 changes: 25 additions & 0 deletions src/v2/controllers/user/rebind-phone/send-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Type } from "@sinclair/typebox";
import { FastifyRequestTypebox, Response } from "../../../../types/Server";
import { UserRebindPhoneService } from "../../../services/user/rebind-phone";
import { successJSON } from "../../internal/utils/response-json";

export const userRebindPhoneSendMessageSchema = {
body: Type.Object(
{
phone: Type.String(),
},
{
additionalProperties: false,
},
),
};

export const userRebindPhoneSendMessage = async (
req: FastifyRequestTypebox<typeof userRebindPhoneSendMessageSchema>,
): Promise<Response> => {
const service = new UserRebindPhoneService(req.ids, req.DBTransaction, req.userUUID);

await service.sendMessage(req.body.phone);

return successJSON({});
};
13 changes: 13 additions & 0 deletions src/v2/controllers/user/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { userRename, userRenameSchema } from "./rename";
import { userUploadAvatarStart, userUploadAvatarStartSchema } from "./upload-avatar/start";
import { userUploadAvatarFinish, userUploadAvatarFinishSchema } from "./upload-avatar/finish";
import { userSensitive, userSensitiveSchema } from "./sensitive";
import {
userRebindPhoneSendMessage,
userRebindPhoneSendMessageSchema,
} from "./rebind-phone/send-message";
import { userRebindPhone, userRebindPhoneSchema } from "./rebind-phone";

export const userRouters = (server: Server): void => {
server.post("user/rename", userRename, {
Expand All @@ -20,4 +25,12 @@ export const userRouters = (server: Server): void => {
server.post("user/sensitive", userSensitive, {
schema: userSensitiveSchema,
});

server.post("user/rebind-phone/send-message", userRebindPhoneSendMessage, {
schema: userRebindPhoneSendMessageSchema,
});

server.post("user/rebind-phone", userRebindPhone, {
schema: userRebindPhoneSchema,
});
};
2 changes: 1 addition & 1 deletion src/v2/dao/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { OAuthSecretsModel } from "../../model/oauth/oauth-secrets";
import { OAuthUsersModel } from "../../model/oauth/oauth-users";
import { dataSource } from "../../thirdPartyService/TypeORMService";

class DAO<M extends Model> {
export class DAO<M extends Model> {
public constructor(private readonly model: EntityTarget<M>) {}

public async findOne<T extends keyof M>(
Expand Down
160 changes: 160 additions & 0 deletions src/v2/services/user/__tests__/rebind-phone.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import test from "ava";
import { v4 } from "uuid";
import { Status } from "../../../../constants/Project";
import { FError } from "../../../../error/ControllerError";
import { ErrorCode } from "../../../../ErrorCode";
import { RoomStatus } from "../../../../model/room/Constants";
import RedisService from "../../../../thirdPartyService/RedisService";
import { RedisKey } from "../../../../utils/Redis";
import { userDAO, userPhoneDAO, userWeChatDAO } from "../../../dao";
import { testService } from "../../../__tests__/helpers/db";
import { useTransaction } from "../../../__tests__/helpers/db/query-runner";
import { initializeDataSource } from "../../../__tests__/helpers/db/test-hooks";
import { randomPhoneNumber } from "../../../__tests__/helpers/db/user-phone";
import { ids } from "../../../__tests__/helpers/fastify/ids";
import { MessageExpirationSecond, UserRebindPhoneService } from "../rebind-phone";

const namespace = "v2.services.user.rebind-phone";
initializeDataSource(test, namespace);

test(`${namespace} - user already bind phone in send message`, async ava => {
const { t, releaseRunner } = await useTransaction();
const { createUser, createUserPhone } = testService(t);

const userInfo = await createUser.quick();
const userPhoneInfo = await createUserPhone.quick(userInfo);

await ava.throwsAsync(
() =>
new UserRebindPhoneService(ids(), t, userInfo.userUUID).sendMessage(
userPhoneInfo.phoneNumber,
),
{
instanceOf: FError,
message: `${Status.Failed}: ${ErrorCode.SMSAlreadyExist}`,
},
);

await releaseRunner();
});

test(`${namespace} - user has joined room in rebind`, async ava => {
const { t, releaseRunner } = await useTransaction();
const { createUser, createUserPhone, createRoom, createRoomJoin } = testService(t);

const userInfo = await createUser.quick();
const roomInfo = await createRoom.quick({ roomStatus: RoomStatus.Started });
await createRoomJoin.quick({ ...roomInfo, ...userInfo });

const targetUserInfo = await createUser.quick();
const targetUserPhoneInfo = await createUserPhone.quick(targetUserInfo);

RedisService.set(
RedisKey.phoneBinding(targetUserPhoneInfo.phoneNumber),
"666666",
MessageExpirationSecond,
);

await ava.throwsAsync(
() =>
new UserRebindPhoneService(ids(), t, userInfo.userUUID).rebind(
targetUserPhoneInfo.phoneNumber,
666666,
async () => "",
),
{
instanceOf: FError,
message: `${Status.Failed}: ${ErrorCode.UserRoomListNotEmpty}`,
},
);

await releaseRunner();
});

test(`${namespace} - user not found in rebind`, async ava => {
const { t, releaseRunner } = await useTransaction();
const { createUser, createUserPhone } = testService(t);

await ava.throwsAsync(
() =>
new UserRebindPhoneService(ids(), t, v4()).rebind(
randomPhoneNumber(),
666666,
async () => "",
),
{
instanceOf: FError,
message: `${Status.Failed}: ${ErrorCode.UserNotFound}`,
},
);

const userInfo = await createUser.quick();
const userPhoneInfo = await createUserPhone.quick(userInfo);

await ava.throwsAsync(
() =>
new UserRebindPhoneService(ids(), t, userInfo.userUUID).rebind(
randomPhoneNumber(),
666666,
async () => "",
),
{
instanceOf: FError,
message: `${Status.Failed}: ${ErrorCode.SMSAlreadyBinding}`,
},
);

await userPhoneDAO.delete(t, { phone_number: userPhoneInfo.phoneNumber });

await ava.throwsAsync(
() =>
new UserRebindPhoneService(ids(), t, userInfo.userUUID).rebind(
randomPhoneNumber(),
666666,
async () => "",
),
{
instanceOf: FError,
message: `${Status.Failed}: ${ErrorCode.UserNotFound}`,
},
);

await releaseRunner();
});

test(`${namespace} - user rebind success`, async ava => {
const { t, releaseRunner } = await useTransaction();
const { createUser, createUserPhone, createUserWeChat } = testService(t);

const userInfo = await createUser.quick();
const oldUserWeChatInfo = await createUserWeChat.quick(userInfo);

const targetUserInfo = await createUser.quick();
const targetUserPhoneInfo = await createUserPhone.quick(targetUserInfo);

RedisService.set(
RedisKey.phoneBinding(targetUserPhoneInfo.phoneNumber),
"666666",
MessageExpirationSecond,
);

const result = await new UserRebindPhoneService(ids(), t, userInfo.userUUID).rebind(
targetUserPhoneInfo.phoneNumber,
666666,
async () => "",
);

ava.deepEqual(result.rebind, { WeChat: 0, Github: -1, Apple: -1, Agora: -1, Google: -1 });

const newUserWeChatInfo = await userWeChatDAO.findOne(t, ["union_uuid", "open_uuid"], {
user_uuid: targetUserInfo.userUUID,
});

ava.is(newUserWeChatInfo?.open_uuid, oldUserWeChatInfo.openUUID);
ava.is(newUserWeChatInfo?.union_uuid, oldUserWeChatInfo.unionUUID);

const userShouldDelete = await userDAO.findOne(t, ["id"], { user_uuid: userInfo.userUUID });
ava.is(userShouldDelete, null);

await releaseRunner();
});
Loading