From f5d10683a1edeba4dabe148ff7aa682c044f7496 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 26 Jul 2020 15:02:55 +0800 Subject: Merge front end repo --- Timeline/ClientApp/src/app/http/mock/timeline.ts | 600 +++++++++++++++++++++++ 1 file changed, 600 insertions(+) create mode 100644 Timeline/ClientApp/src/app/http/mock/timeline.ts (limited to 'Timeline/ClientApp/src/app/http/mock/timeline.ts') diff --git a/Timeline/ClientApp/src/app/http/mock/timeline.ts b/Timeline/ClientApp/src/app/http/mock/timeline.ts new file mode 100644 index 00000000..2a34ef10 --- /dev/null +++ b/Timeline/ClientApp/src/app/http/mock/timeline.ts @@ -0,0 +1,600 @@ +import { random, without, range } from 'lodash'; + +import { BlobWithEtag, NotModified } from '../common'; +import { + IHttpTimelineClient, + HttpTimelineInfo, + TimelineVisibility, + HttpTimelineListQuery, + HttpTimelineNotExistError, + HttpTimelinePostRequest, + HttpTimelineNameConflictError, + HttpTimelinePatchRequest, + HttpTimelinePostInfo, + HttpTimelinePostContent, + HttpTimelinePostPostRequest, + HttpTimelinePostNotExistError, + HttpTimelineGenericPostInfo, +} from '../timeline'; +import { HttpUser } from '../user'; + +import { mockStorage, sha1, mockPrepare } from './common'; +import { getUser, MockUserNotExistError, checkToken } from './user'; + +async function getTimelineNameList(): Promise { + return (await mockStorage.getItem('timelines')) ?? []; +} + +async function setTimelineNameList(newOne: string[]): Promise { + await mockStorage.setItem('timelines', newOne); +} + +type TimelinePropertyKey = + | 'uniqueId' + | 'owner' + | 'description' + | 'visibility' + | 'members' + | 'currentPostId'; + +function getTimelinePropertyKey( + name: string, + property: TimelinePropertyKey +): string { + return `timeline.${name}.${property}`; +} + +function getTimelinePropertyValue( + name: string, + property: TimelinePropertyKey +): Promise { + return mockStorage.getItem(getTimelinePropertyKey(name, property)); +} + +function setTimelinePropertyValue( + name: string, + property: TimelinePropertyKey, + value: T +): Promise { + return mockStorage + .setItem(getTimelinePropertyKey(name, property), value) + .then(); +} + +interface HttpTimelineInfoEx extends HttpTimelineInfo { + memberUsernames: string[]; +} + +function createUniqueId(): string { + const s = 'abcdefghijklmnopqrstuvwxz0123456789'; + let result = ''; + for (let i = 0; i < 16; i++) { + result += s[random(0, s.length - 1)]; + } + return result; +} + +class MockTimelineNotExistError extends Error { + constructor() { + super('Timeline not exist.'); + } +} + +class MockTimelineAlreadyExistError extends Error { + constructor() { + super('Timeline already exist.'); + } +} + +async function getTimelineInfo(name: string): Promise { + let owner: HttpUser; + if (name.startsWith('@')) { + const ownerUsername = name.substr(1); + owner = await getUser(ownerUsername); + const optionalUniqueId = await getTimelinePropertyValue( + name, + 'uniqueId' + ); + if (optionalUniqueId == null) { + await setTimelineNameList([...(await getTimelineNameList()), name]); + await setTimelinePropertyValue(name, 'uniqueId', createUniqueId()); + } + } else { + const optionalOwnerUsername = await getTimelinePropertyValue( + name, + 'owner' + ); + if (optionalOwnerUsername == null) { + throw new MockTimelineNotExistError(); + } else { + owner = await getUser(optionalOwnerUsername); + } + } + + const memberUsernames = + (await getTimelinePropertyValue(name, 'members')) ?? []; + const members = await Promise.all( + memberUsernames.map(async (username) => { + return await getUser(username); + }) + ); + + return { + name, + uniqueId: await getTimelinePropertyValue(name, 'uniqueId'), + owner, + description: + (await getTimelinePropertyValue(name, 'description')) ?? + '', + visibility: + (await getTimelinePropertyValue( + name, + 'visibility' + )) ?? 'Register', + members, + memberUsernames, + }; +} + +async function createTimeline(name: string, owner: string): Promise { + const optionalOwnerUsername = await getTimelinePropertyValue( + name, + 'owner' + ); + if (optionalOwnerUsername != null) { + throw new MockTimelineAlreadyExistError(); + } + + await setTimelineNameList([...(await getTimelineNameList()), name]); + await setTimelinePropertyValue(name, 'uniqueId', createUniqueId()); + await setTimelinePropertyValue(name, 'owner', owner); +} + +type TimelinePostPropertyKey = + | 'type' + | 'data' + | 'etag' + | 'author' + | 'time' + | 'lastUpdated'; + +function getTimelinePostPropertyKey( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey +): string { + return `timeline.${timelineName}.posts.${id}.${propertyKey}`; +} + +function getTimelinePostPropertyValue( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey +): Promise { + return mockStorage.getItem( + getTimelinePostPropertyKey(timelineName, id, propertyKey) + ); +} + +function setTimelinePostPropertyValue( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey, + value: T +): Promise { + return mockStorage.setItem( + getTimelinePostPropertyKey(timelineName, id, propertyKey), + value + ); +} + +function removeTimelinePostProperty( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey +): Promise { + return mockStorage.removeItem( + getTimelinePostPropertyKey(timelineName, id, propertyKey) + ); +} + +async function getTimelinePostInfo( + timelineName: string, + id: number +): Promise { + const currentPostId = await getTimelinePropertyValue( + timelineName, + 'currentPostId' + ); + if (currentPostId == null || id > currentPostId) { + throw new HttpTimelinePostNotExistError(); + } + + const type = await getTimelinePostPropertyValue( + timelineName, + id, + 'type' + ); + + if (type == null) { + return { + id, + author: await getUser( + await getTimelinePostPropertyValue(timelineName, id, 'author') + ), + time: new Date( + await getTimelinePostPropertyValue(timelineName, id, 'time') + ), + lastUpdated: new Date( + await getTimelinePostPropertyValue( + timelineName, + id, + 'lastUpdated' + ) + ), + deleted: true, + }; + } else { + let content: HttpTimelinePostContent; + if (type === 'text') { + content = { + type: 'text', + text: await getTimelinePostPropertyValue(timelineName, id, 'data'), + }; + } else { + content = { + type: 'image', + }; + } + + return { + id, + author: await getUser( + await getTimelinePostPropertyValue(timelineName, id, 'author') + ), + time: new Date( + await getTimelinePostPropertyValue(timelineName, id, 'time') + ), + lastUpdated: new Date( + await getTimelinePostPropertyValue( + timelineName, + id, + 'lastUpdated' + ) + ), + content, + deleted: false, + }; + } +} + +export class MockHttpTimelineClient implements IHttpTimelineClient { + async listTimeline( + query: HttpTimelineListQuery + ): Promise { + await mockPrepare(); + return ( + await Promise.all( + (await getTimelineNameList()).map((name) => getTimelineInfo(name)) + ) + ).filter((timeline) => { + if ( + query.visibility != null && + query.visibility !== timeline.visibility + ) { + return false; + } + if (query.relate != null) { + if (query.relateType === 'own') { + if (timeline.owner.username !== query.relate) { + return false; + } + } else if (query.relateType === 'join') { + if (!timeline.memberUsernames.includes(query.relate)) { + return false; + } + } else if ( + timeline.owner.username !== query.relate && + !timeline.memberUsernames.includes(query.relate) + ) { + return false; + } + } + return true; + }); + } + + async getTimeline(timelineName: string): Promise { + await mockPrepare(); + try { + return await getTimelineInfo(timelineName); + } catch (e) { + if ( + e instanceof MockTimelineNotExistError || + e instanceof MockUserNotExistError + ) { + throw new HttpTimelineNotExistError(); + } + throw e; + } + } + + async postTimeline( + req: HttpTimelinePostRequest, + token: string + ): Promise { + await mockPrepare(); + const user = checkToken(token); + try { + await createTimeline(req.name, user); + } catch (e) { + if (e instanceof MockTimelineAlreadyExistError) { + throw new HttpTimelineNameConflictError(); + } + throw e; + } + return await getTimelineInfo(req.name); + } + + async patchTimeline( + timelineName: string, + req: HttpTimelinePatchRequest, + _token: string + ): Promise { + await mockPrepare(); + if (req.description != null) { + await setTimelinePropertyValue( + timelineName, + 'description', + req.description + ); + } + if (req.visibility != null) { + await setTimelinePropertyValue( + timelineName, + 'visibility', + req.visibility + ); + } + return await getTimelineInfo(timelineName); + } + + async deleteTimeline(timelineName: string, _token: string): Promise { + await mockPrepare(); + await setTimelineNameList( + without(await getTimelineNameList(), timelineName) + ); + await mockStorage.removeItem( + getTimelinePropertyKey(timelineName, 'uniqueId') + ); + + // TODO: remove other things + } + + async memberPut( + timelineName: string, + username: string, + _token: string + ): Promise { + await mockPrepare(); + const oldMembers = + (await getTimelinePropertyValue( + timelineName, + 'members' + )) ?? []; + if (!oldMembers.includes(username)) { + await setTimelinePropertyValue(timelineName, 'members', [ + ...oldMembers, + username, + ]); + } + } + + async memberDelete( + timelineName: string, + username: string, + _token: string + ): Promise { + await mockPrepare(); + const oldMembers = + (await getTimelinePropertyValue( + timelineName, + 'members' + )) ?? []; + if (oldMembers.includes(username)) { + await setTimelinePropertyValue( + timelineName, + 'members', + without(oldMembers, username) + ); + } + } + + listPost( + timelineName: string, + token?: string + ): Promise; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted?: false; + } + ): Promise; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted: true; + } + ): Promise; + async listPost( + timelineName: string, + _token?: string, + query?: { + modifiedSince?: Date; + includeDeleted?: boolean; + } + ): Promise { + await mockPrepare(); + // TODO: Permission check. + + const currentPostId = await getTimelinePropertyValue( + timelineName, + 'currentPostId' + ); + + return ( + await Promise.all( + range(1, currentPostId == null ? 1 : currentPostId + 1).map( + async (id) => { + return await getTimelinePostInfo(timelineName, id); + } + ) + ) + ) + .filter((post) => { + if (query?.includeDeleted !== true && post.deleted) { + return false; + } + return true; + }) + .filter((post) => { + if (query?.modifiedSince != null) { + return post.lastUpdated >= query.modifiedSince; + } + return true; + }); + } + + getPostData( + timelineName: string, + postId: number, + token: string + ): Promise; + async getPostData( + timelineName: string, + postId: number, + _token?: string, + etag?: string + ): Promise { + await mockPrepare(); + // TODO: Permission check. + + const optionalSavedEtag = await getTimelinePostPropertyValue( + timelineName, + postId, + 'etag' + ); + + if (optionalSavedEtag == null) { + const optionalType = await getTimelinePostPropertyValue( + timelineName, + postId, + 'type' + ); + + if (optionalType != null) { + throw new Error('Post of this type has no data.'); + } else { + throw new HttpTimelinePostNotExistError(); + } + } + + if (etag === optionalSavedEtag) { + return new NotModified(); + } + + return { + data: await getTimelinePostPropertyValue( + timelineName, + postId, + 'data' + ), + etag: optionalSavedEtag, + }; + } + + async postPost( + timelineName: string, + req: HttpTimelinePostPostRequest, + token: string + ): Promise { + await mockPrepare(); + const user = checkToken(token); + + const savedId = await getTimelinePropertyValue( + timelineName, + 'currentPostId' + ); + const id = savedId ? savedId + 1 : 1; + await setTimelinePropertyValue(timelineName, 'currentPostId', id); + + await setTimelinePostPropertyValue(timelineName, id, 'author', user); + + const currentTimeString = new Date().toISOString(); + await setTimelinePostPropertyValue( + timelineName, + id, + 'lastUpdated', + currentTimeString + ); + + await setTimelinePostPropertyValue( + timelineName, + id, + 'time', + req.time != null ? req.time.toISOString() : currentTimeString + ); + + const { content } = req; + if (content.type === 'text') { + await setTimelinePostPropertyValue(timelineName, id, 'type', 'text'); + await setTimelinePostPropertyValue( + timelineName, + id, + 'data', + content.text + ); + } else { + await setTimelinePostPropertyValue(timelineName, id, 'type', 'image'); + await setTimelinePostPropertyValue( + timelineName, + id, + 'data', + content.data + ); + await setTimelinePostPropertyValue( + timelineName, + id, + 'etag', + await sha1(content.data) + ); + } + + return (await getTimelinePostInfo( + timelineName, + id + )) as HttpTimelinePostInfo; + } + + async deletePost( + timelineName: string, + postId: number, + _token: string + ): Promise { + await mockPrepare(); + // TODO: permission check + await removeTimelinePostProperty(timelineName, postId, 'type'); + await removeTimelinePostProperty(timelineName, postId, 'data'); + await removeTimelinePostProperty(timelineName, postId, 'etag'); + await setTimelinePostPropertyValue( + timelineName, + postId, + 'lastUpdated', + new Date().toISOString() + ); + } +} -- cgit v1.2.3