diff options
| author | crupest <crupest@outlook.com> | 2020-07-26 15:02:55 +0800 | 
|---|---|---|
| committer | crupest <crupest@outlook.com> | 2020-07-26 15:02:55 +0800 | 
| commit | f5d10683a1edeba4dabe148ff7aa682c044f7496 (patch) | |
| tree | d8f7edae96baa26823dee80ccc9329a23ac04c3c /Timeline/ClientApp/src/app/http/mock | |
| parent | 7753c9cad23b06c2acdd908a5a7cc3863bfa6b61 (diff) | |
| download | timeline-f5d10683a1edeba4dabe148ff7aa682c044f7496.tar.gz timeline-f5d10683a1edeba4dabe148ff7aa682c044f7496.tar.bz2 timeline-f5d10683a1edeba4dabe148ff7aa682c044f7496.zip  | |
Merge front end repo
Diffstat (limited to 'Timeline/ClientApp/src/app/http/mock')
| -rw-r--r-- | Timeline/ClientApp/src/app/http/mock/common.ts | 48 | ||||
| -rw-r--r-- | Timeline/ClientApp/src/app/http/mock/default-avatar.png | bin | 0 -> 26442 bytes | |||
| -rw-r--r-- | Timeline/ClientApp/src/app/http/mock/install.ts | 11 | ||||
| -rw-r--r-- | Timeline/ClientApp/src/app/http/mock/timeline.ts | 600 | ||||
| -rw-r--r-- | Timeline/ClientApp/src/app/http/mock/token.ts | 53 | ||||
| -rw-r--r-- | Timeline/ClientApp/src/app/http/mock/user.ts | 132 | 
6 files changed, 844 insertions, 0 deletions
diff --git a/Timeline/ClientApp/src/app/http/mock/common.ts b/Timeline/ClientApp/src/app/http/mock/common.ts new file mode 100644 index 00000000..11939c2b --- /dev/null +++ b/Timeline/ClientApp/src/app/http/mock/common.ts @@ -0,0 +1,48 @@ +import localforage from 'localforage';
 +import { SHA1 } from 'crypto-js';
 +
 +import { HttpNetworkError } from '../common';
 +
 +export const mockStorage = localforage.createInstance({
 +  name: 'mock-backend',
 +  description: 'Database for mock back end.',
 +  driver: localforage.INDEXEDDB,
 +});
 +
 +export async function sha1(data: Blob): Promise<string> {
 +  const s = await new Promise<string>((resolve) => {
 +    const fileReader = new FileReader();
 +    fileReader.readAsBinaryString(data);
 +    fileReader.onload = () => {
 +      resolve(fileReader.result as string);
 +    };
 +  });
 +
 +  return SHA1(s).toString();
 +}
 +
 +const disableNetworkKey = 'mockServer.disableNetwork';
 +
 +let disableNetwork: boolean =
 +  localStorage.getItem(disableNetworkKey) === 'true' ? true : false;
 +
 +Object.defineProperty(window, 'disableNetwork', {
 +  get: () => disableNetwork,
 +  set: (value) => {
 +    if (value) {
 +      disableNetwork = true;
 +      localStorage.setItem(disableNetworkKey, 'true');
 +    } else {
 +      disableNetwork = false;
 +      localStorage.setItem(disableNetworkKey, 'false');
 +    }
 +  },
 +});
 +
 +export async function mockPrepare(): Promise<void> {
 +  if (disableNetwork) {
 +    console.warn('Network is disabled for mock server.');
 +    throw new HttpNetworkError();
 +  }
 +  await Promise.resolve();
 +}
 diff --git a/Timeline/ClientApp/src/app/http/mock/default-avatar.png b/Timeline/ClientApp/src/app/http/mock/default-avatar.png Binary files differnew file mode 100644 index 00000000..4086e1d2 --- /dev/null +++ b/Timeline/ClientApp/src/app/http/mock/default-avatar.png diff --git a/Timeline/ClientApp/src/app/http/mock/install.ts b/Timeline/ClientApp/src/app/http/mock/install.ts new file mode 100644 index 00000000..66174d41 --- /dev/null +++ b/Timeline/ClientApp/src/app/http/mock/install.ts @@ -0,0 +1,11 @@ +import { setHttpTokenClient } from '../token';
 +import { setHttpUserClient } from '../user';
 +import { setHttpTimelineClient } from '../timeline';
 +
 +import { MockHttpTokenClient } from './token';
 +import { MockHttpUserClient } from './user';
 +import { MockHttpTimelineClient } from './timeline';
 +
 +setHttpTokenClient(new MockHttpTokenClient());
 +setHttpUserClient(new MockHttpUserClient());
 +setHttpTimelineClient(new MockHttpTimelineClient());
 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<string[]> {
 +  return (await mockStorage.getItem<string[]>('timelines')) ?? [];
 +}
 +
 +async function setTimelineNameList(newOne: string[]): Promise<void> {
 +  await mockStorage.setItem<string[]>('timelines', newOne);
 +}
 +
 +type TimelinePropertyKey =
 +  | 'uniqueId'
 +  | 'owner'
 +  | 'description'
 +  | 'visibility'
 +  | 'members'
 +  | 'currentPostId';
 +
 +function getTimelinePropertyKey(
 +  name: string,
 +  property: TimelinePropertyKey
 +): string {
 +  return `timeline.${name}.${property}`;
 +}
 +
 +function getTimelinePropertyValue<T>(
 +  name: string,
 +  property: TimelinePropertyKey
 +): Promise<T> {
 +  return mockStorage.getItem<T>(getTimelinePropertyKey(name, property));
 +}
 +
 +function setTimelinePropertyValue<T>(
 +  name: string,
 +  property: TimelinePropertyKey,
 +  value: T
 +): Promise<void> {
 +  return mockStorage
 +    .setItem<T>(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<HttpTimelineInfoEx> {
 +  let owner: HttpUser;
 +  if (name.startsWith('@')) {
 +    const ownerUsername = name.substr(1);
 +    owner = await getUser(ownerUsername);
 +    const optionalUniqueId = await getTimelinePropertyValue<string | null>(
 +      name,
 +      'uniqueId'
 +    );
 +    if (optionalUniqueId == null) {
 +      await setTimelineNameList([...(await getTimelineNameList()), name]);
 +      await setTimelinePropertyValue(name, 'uniqueId', createUniqueId());
 +    }
 +  } else {
 +    const optionalOwnerUsername = await getTimelinePropertyValue<string | null>(
 +      name,
 +      'owner'
 +    );
 +    if (optionalOwnerUsername == null) {
 +      throw new MockTimelineNotExistError();
 +    } else {
 +      owner = await getUser(optionalOwnerUsername);
 +    }
 +  }
 +
 +  const memberUsernames =
 +    (await getTimelinePropertyValue<string[] | null>(name, 'members')) ?? [];
 +  const members = await Promise.all(
 +    memberUsernames.map(async (username) => {
 +      return await getUser(username);
 +    })
 +  );
 +
 +  return {
 +    name,
 +    uniqueId: await getTimelinePropertyValue<string>(name, 'uniqueId'),
 +    owner,
 +    description:
 +      (await getTimelinePropertyValue<string | null>(name, 'description')) ??
 +      '',
 +    visibility:
 +      (await getTimelinePropertyValue<TimelineVisibility | null>(
 +        name,
 +        'visibility'
 +      )) ?? 'Register',
 +    members,
 +    memberUsernames,
 +  };
 +}
 +
 +async function createTimeline(name: string, owner: string): Promise<void> {
 +  const optionalOwnerUsername = await getTimelinePropertyValue<string | null>(
 +    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<T>(
 +  timelineName: string,
 +  id: number,
 +  propertyKey: TimelinePostPropertyKey
 +): Promise<T> {
 +  return mockStorage.getItem<T>(
 +    getTimelinePostPropertyKey(timelineName, id, propertyKey)
 +  );
 +}
 +
 +function setTimelinePostPropertyValue<T>(
 +  timelineName: string,
 +  id: number,
 +  propertyKey: TimelinePostPropertyKey,
 +  value: T
 +): Promise<T> {
 +  return mockStorage.setItem(
 +    getTimelinePostPropertyKey(timelineName, id, propertyKey),
 +    value
 +  );
 +}
 +
 +function removeTimelinePostProperty(
 +  timelineName: string,
 +  id: number,
 +  propertyKey: TimelinePostPropertyKey
 +): Promise<void> {
 +  return mockStorage.removeItem(
 +    getTimelinePostPropertyKey(timelineName, id, propertyKey)
 +  );
 +}
 +
 +async function getTimelinePostInfo(
 +  timelineName: string,
 +  id: number
 +): Promise<HttpTimelineGenericPostInfo> {
 +  const currentPostId = await getTimelinePropertyValue<number | null>(
 +    timelineName,
 +    'currentPostId'
 +  );
 +  if (currentPostId == null || id > currentPostId) {
 +    throw new HttpTimelinePostNotExistError();
 +  }
 +
 +  const type = await getTimelinePostPropertyValue<string | null>(
 +    timelineName,
 +    id,
 +    'type'
 +  );
 +
 +  if (type == null) {
 +    return {
 +      id,
 +      author: await getUser(
 +        await getTimelinePostPropertyValue<string>(timelineName, id, 'author')
 +      ),
 +      time: new Date(
 +        await getTimelinePostPropertyValue<string>(timelineName, id, 'time')
 +      ),
 +      lastUpdated: new Date(
 +        await getTimelinePostPropertyValue<string>(
 +          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<string>(timelineName, id, 'author')
 +      ),
 +      time: new Date(
 +        await getTimelinePostPropertyValue<string>(timelineName, id, 'time')
 +      ),
 +      lastUpdated: new Date(
 +        await getTimelinePostPropertyValue<string>(
 +          timelineName,
 +          id,
 +          'lastUpdated'
 +        )
 +      ),
 +      content,
 +      deleted: false,
 +    };
 +  }
 +}
 +
 +export class MockHttpTimelineClient implements IHttpTimelineClient {
 +  async listTimeline(
 +    query: HttpTimelineListQuery
 +  ): Promise<HttpTimelineInfo[]> {
 +    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<HttpTimelineInfo> {
 +    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<HttpTimelineInfo> {
 +    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<HttpTimelineInfo> {
 +    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<void> {
 +    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<void> {
 +    await mockPrepare();
 +    const oldMembers =
 +      (await getTimelinePropertyValue<string[] | null>(
 +        timelineName,
 +        'members'
 +      )) ?? [];
 +    if (!oldMembers.includes(username)) {
 +      await setTimelinePropertyValue(timelineName, 'members', [
 +        ...oldMembers,
 +        username,
 +      ]);
 +    }
 +  }
 +
 +  async memberDelete(
 +    timelineName: string,
 +    username: string,
 +    _token: string
 +  ): Promise<void> {
 +    await mockPrepare();
 +    const oldMembers =
 +      (await getTimelinePropertyValue<string[] | null>(
 +        timelineName,
 +        'members'
 +      )) ?? [];
 +    if (oldMembers.includes(username)) {
 +      await setTimelinePropertyValue(
 +        timelineName,
 +        'members',
 +        without(oldMembers, username)
 +      );
 +    }
 +  }
 +
 +  listPost(
 +    timelineName: string,
 +    token?: string
 +  ): Promise<HttpTimelinePostInfo[]>;
 +  listPost(
 +    timelineName: string,
 +    token: string | undefined,
 +    query: {
 +      modifiedSince?: Date;
 +      includeDeleted?: false;
 +    }
 +  ): Promise<HttpTimelinePostInfo[]>;
 +  listPost(
 +    timelineName: string,
 +    token: string | undefined,
 +    query: {
 +      modifiedSince?: Date;
 +      includeDeleted: true;
 +    }
 +  ): Promise<HttpTimelineGenericPostInfo[]>;
 +  async listPost(
 +    timelineName: string,
 +    _token?: string,
 +    query?: {
 +      modifiedSince?: Date;
 +      includeDeleted?: boolean;
 +    }
 +  ): Promise<HttpTimelineGenericPostInfo[]> {
 +    await mockPrepare();
 +    // TODO: Permission check.
 +
 +    const currentPostId = await getTimelinePropertyValue<number | null>(
 +      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<BlobWithEtag>;
 +  async getPostData(
 +    timelineName: string,
 +    postId: number,
 +    _token?: string,
 +    etag?: string
 +  ): Promise<BlobWithEtag | NotModified> {
 +    await mockPrepare();
 +    // TODO: Permission check.
 +
 +    const optionalSavedEtag = await getTimelinePostPropertyValue<string>(
 +      timelineName,
 +      postId,
 +      'etag'
 +    );
 +
 +    if (optionalSavedEtag == null) {
 +      const optionalType = await getTimelinePostPropertyValue<string>(
 +        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<Blob>(
 +        timelineName,
 +        postId,
 +        'data'
 +      ),
 +      etag: optionalSavedEtag,
 +    };
 +  }
 +
 +  async postPost(
 +    timelineName: string,
 +    req: HttpTimelinePostPostRequest,
 +    token: string
 +  ): Promise<HttpTimelinePostInfo> {
 +    await mockPrepare();
 +    const user = checkToken(token);
 +
 +    const savedId = await getTimelinePropertyValue<number | null>(
 +      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<void> {
 +    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()
 +    );
 +  }
 +}
 diff --git a/Timeline/ClientApp/src/app/http/mock/token.ts b/Timeline/ClientApp/src/app/http/mock/token.ts new file mode 100644 index 00000000..6924e7d7 --- /dev/null +++ b/Timeline/ClientApp/src/app/http/mock/token.ts @@ -0,0 +1,53 @@ +import { AxiosError } from 'axios';
 +
 +import {
 +  IHttpTokenClient,
 +  HttpCreateTokenRequest,
 +  HttpCreateTokenResponse,
 +  HttpVerifyTokenRequest,
 +  HttpVerifyTokenResponse,
 +} from '../token';
 +
 +import { mockPrepare } from './common';
 +import { getUser, MockUserNotExistError, checkToken } from './user';
 +
 +export class MockHttpTokenClient implements IHttpTokenClient {
 +  // TODO: Mock bad credentials error.
 +  async create(req: HttpCreateTokenRequest): Promise<HttpCreateTokenResponse> {
 +    await mockPrepare();
 +    try {
 +      const user = await getUser(req.username);
 +      return {
 +        user,
 +        token: `token-${req.username}`,
 +      };
 +    } catch (e) {
 +      if (e instanceof MockUserNotExistError) {
 +        throw {
 +          isAxiosError: true,
 +          response: {
 +            status: 400,
 +          },
 +        } as Partial<AxiosError>;
 +      }
 +      throw e;
 +    }
 +  }
 +
 +  async verify(req: HttpVerifyTokenRequest): Promise<HttpVerifyTokenResponse> {
 +    await mockPrepare();
 +    try {
 +      const user = await getUser(checkToken(req.token));
 +      return {
 +        user,
 +      };
 +    } catch (e) {
 +      throw {
 +        isAxiosError: true,
 +        response: {
 +          status: 400,
 +        },
 +      } as Partial<AxiosError>;
 +    }
 +  }
 +}
 diff --git a/Timeline/ClientApp/src/app/http/mock/user.ts b/Timeline/ClientApp/src/app/http/mock/user.ts new file mode 100644 index 00000000..d16302d4 --- /dev/null +++ b/Timeline/ClientApp/src/app/http/mock/user.ts @@ -0,0 +1,132 @@ +import axios from 'axios';
 +
 +import { BlobWithEtag, NotModified } from '../common';
 +import {
 +  IHttpUserClient,
 +  HttpUser,
 +  HttpUserNotExistError,
 +  HttpUserPatchRequest,
 +  HttpChangePasswordRequest,
 +} from '../user';
 +
 +import { mockStorage, sha1, mockPrepare } from './common';
 +
 +import defaultAvatarUrl from './default-avatar.png';
 +
 +let _defaultAvatar: BlobWithEtag | undefined = undefined;
 +
 +async function getDefaultAvatar(): Promise<BlobWithEtag> {
 +  if (_defaultAvatar == null) {
 +    const blob = (
 +      await axios.get<Blob>(defaultAvatarUrl, {
 +        responseType: 'blob',
 +      })
 +    ).data;
 +    const etag = await sha1(blob);
 +    _defaultAvatar = {
 +      data: blob,
 +      etag,
 +    };
 +  }
 +  return _defaultAvatar;
 +}
 +
 +export class MockTokenError extends Error {
 +  constructor() {
 +    super('Token bad format.');
 +  }
 +}
 +
 +export class MockUserNotExistError extends Error {
 +  constructor() {
 +    super('Only two user "user" and "admin".');
 +  }
 +}
 +
 +export function checkUsername(username: string): void {
 +  if (!['user', 'admin'].includes(username)) throw new MockUserNotExistError();
 +}
 +
 +export function checkToken(token: string): string {
 +  if (!token.startsWith('token-')) {
 +    throw new MockTokenError();
 +  }
 +  return token.substr(6);
 +}
 +
 +export async function getUser(
 +  username: 'user' | 'admin' | string
 +): Promise<HttpUser> {
 +  checkUsername(username);
 +  const savedNickname = await mockStorage.getItem<string>(
 +    `user.${username}.nickname`
 +  );
 +  return {
 +    username: username,
 +    nickname:
 +      savedNickname == null || savedNickname === '' ? username : savedNickname,
 +    administrator: username === 'admin',
 +  };
 +}
 +
 +export class MockHttpUserClient implements IHttpUserClient {
 +  async get(username: string): Promise<HttpUser> {
 +    await mockPrepare();
 +    return await getUser(username).catch((e) => {
 +      if (e instanceof MockUserNotExistError) {
 +        throw new HttpUserNotExistError();
 +      } else {
 +        throw e;
 +      }
 +    });
 +  }
 +
 +  async patch(
 +    username: string,
 +    req: HttpUserPatchRequest,
 +    _token: string
 +  ): Promise<HttpUser> {
 +    await mockPrepare();
 +    if (req.nickname != null) {
 +      await mockStorage.setItem(`user.${username}.nickname`, req.nickname);
 +    }
 +    return await getUser(username);
 +  }
 +
 +  getAvatar(username: string): Promise<BlobWithEtag>;
 +  async getAvatar(
 +    username: string,
 +    etag?: string
 +  ): Promise<BlobWithEtag | NotModified> {
 +    await mockPrepare();
 +
 +    const savedEtag = await mockStorage.getItem(`user.${username}.avatar.etag`);
 +    if (savedEtag == null) {
 +      return await getDefaultAvatar();
 +    }
 +
 +    if (savedEtag === etag) {
 +      return new NotModified();
 +    }
 +
 +    return {
 +      data: await mockStorage.getItem<Blob>(`user.${username}.avatar.data`),
 +      etag: await mockStorage.getItem<string>(`user.${username}.avatar.etag`),
 +    };
 +  }
 +
 +  async putAvatar(username: string, data: Blob, _token: string): Promise<void> {
 +    await mockPrepare();
 +    const etag = await sha1(data);
 +    await mockStorage.setItem<Blob>(`user.${username}.avatar.data`, data);
 +    await mockStorage.setItem<string>(`user.${username}.avatar.etag`, etag);
 +  }
 +
 +  async changePassword(
 +    _req: HttpChangePasswordRequest,
 +    _token: string
 +  ): Promise<void> {
 +    await mockPrepare();
 +    throw new Error('Not Implemented.');
 +  }
 +}
  | 
