diff options
| author | crupest <crupest@outlook.com> | 2020-08-25 00:05:01 +0800 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-08-25 00:05:01 +0800 | 
| commit | 738a17688ca481db62755295f8b732200ca12511 (patch) | |
| tree | 034b470eff54a9f725cd9e472966cf554cd2ee26 /Timeline/ClientApp/src/app/http/mock | |
| parent | b9b7334ac5d5cf83b6206af024ddd4c55a6e07da (diff) | |
| parent | fe687f82d9c5af2869b679222bec11b1a00b9ee9 (diff) | |
| download | timeline-738a17688ca481db62755295f8b732200ca12511.tar.gz timeline-738a17688ca481db62755295f8b732200ca12511.tar.bz2 timeline-738a17688ca481db62755295f8b732200ca12511.zip | |
Merge pull request #154 from crupest/toolchain
Upgrade toolchain of front end.
Diffstat (limited to 'Timeline/ClientApp/src/app/http/mock')
| -rw-r--r-- | Timeline/ClientApp/src/app/http/mock/common.ts | 156 | ||||
| -rw-r--r-- | Timeline/ClientApp/src/app/http/mock/install.ts | 22 | ||||
| -rw-r--r-- | Timeline/ClientApp/src/app/http/mock/timeline.ts | 1318 | ||||
| -rw-r--r-- | Timeline/ClientApp/src/app/http/mock/token.ts | 106 | ||||
| -rw-r--r-- | Timeline/ClientApp/src/app/http/mock/user.ts | 279 | 
5 files changed, 941 insertions, 940 deletions
| diff --git a/Timeline/ClientApp/src/app/http/mock/common.ts b/Timeline/ClientApp/src/app/http/mock/common.ts index 3dce8117..787d81bd 100644 --- a/Timeline/ClientApp/src/app/http/mock/common.ts +++ b/Timeline/ClientApp/src/app/http/mock/common.ts @@ -1,78 +1,78 @@ -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';
 -const networkLatencyKey = 'mockServer.networkLatency';
 -
 -let disableNetwork: boolean =
 -  localStorage.getItem(disableNetworkKey) === 'true' ? true : false;
 -
 -const savedNetworkLatency = localStorage.getItem(networkLatencyKey);
 -
 -let networkLatency: number | null =
 -  savedNetworkLatency != null ? Number(savedNetworkLatency) : null;
 -
 -Object.defineProperty(window, 'disableNetwork', {
 -  get: () => disableNetwork,
 -  set: (value) => {
 -    if (value) {
 -      disableNetwork = true;
 -      localStorage.setItem(disableNetworkKey, 'true');
 -    } else {
 -      disableNetwork = false;
 -      localStorage.setItem(disableNetworkKey, 'false');
 -    }
 -  },
 -});
 -
 -Object.defineProperty(window, 'networkLatency', {
 -  get: () => networkLatency,
 -  set: (value) => {
 -    if (typeof value === 'number') {
 -      networkLatency = value;
 -      localStorage.setItem(networkLatencyKey, value.toString());
 -    } else if (value == null) {
 -      networkLatency = null;
 -      localStorage.removeItem(networkLatencyKey);
 -    }
 -  },
 -});
 -
 -export async function mockPrepare(key: string): Promise<void> {
 -  console.log(`Recieve request: ${key}`);
 -
 -  if (disableNetwork) {
 -    console.warn('Network is disabled for mock server.');
 -    throw new HttpNetworkError();
 -  }
 -  if (networkLatency != null) {
 -    await new Promise((resolve) => {
 -      window.setTimeout(() => {
 -        resolve();
 -        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 -      }, networkLatency! * 1000);
 -    });
 -  }
 -
 -  await Promise.resolve();
 -}
 +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"; +const networkLatencyKey = "mockServer.networkLatency"; + +let disableNetwork: boolean = +  localStorage.getItem(disableNetworkKey) === "true" ? true : false; + +const savedNetworkLatency = localStorage.getItem(networkLatencyKey); + +let networkLatency: number | null = +  savedNetworkLatency != null ? Number(savedNetworkLatency) : null; + +Object.defineProperty(window, "disableNetwork", { +  get: () => disableNetwork, +  set: (value) => { +    if (value) { +      disableNetwork = true; +      localStorage.setItem(disableNetworkKey, "true"); +    } else { +      disableNetwork = false; +      localStorage.setItem(disableNetworkKey, "false"); +    } +  }, +}); + +Object.defineProperty(window, "networkLatency", { +  get: () => networkLatency, +  set: (value) => { +    if (typeof value === "number") { +      networkLatency = value; +      localStorage.setItem(networkLatencyKey, value.toString()); +    } else if (value == null) { +      networkLatency = null; +      localStorage.removeItem(networkLatencyKey); +    } +  }, +}); + +export async function mockPrepare(key: string): Promise<void> { +  console.log(`Recieve request: ${key}`); + +  if (disableNetwork) { +    console.warn("Network is disabled for mock server."); +    throw new HttpNetworkError(); +  } +  if (networkLatency != null) { +    await new Promise((resolve) => { +      window.setTimeout(() => { +        resolve(); +        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion +      }, networkLatency! * 1000); +    }); +  } + +  await Promise.resolve(); +} diff --git a/Timeline/ClientApp/src/app/http/mock/install.ts b/Timeline/ClientApp/src/app/http/mock/install.ts index 66174d41..17b7cc13 100644 --- a/Timeline/ClientApp/src/app/http/mock/install.ts +++ b/Timeline/ClientApp/src/app/http/mock/install.ts @@ -1,11 +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());
 +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 index 9434f666..27addd61 100644 --- a/Timeline/ClientApp/src/app/http/mock/timeline.ts +++ b/Timeline/ClientApp/src/app/http/mock/timeline.ts @@ -1,658 +1,660 @@ -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'
 -  | 'lastModified'
 -  | '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();
 -}
 -
 -function updateTimelineLastModified(name: string): Promise<void> {
 -  return setTimelinePropertyValue(
 -    name,
 -    'lastModified',
 -    new Date().toISOString()
 -  );
 -}
 -
 -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());
 -      await updateTimelineLastModified(name);
 -    }
 -  } 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',
 -    lastModified: new Date(
 -      await getTimelinePropertyValue<string>(name, 'lastModified')
 -    ),
 -    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);
 -  await updateTimelineLastModified(name);
 -}
 -
 -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('timeline.list');
 -    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;
 -    });
 -  }
 -
 -  getTimeline(timelineName: string): Promise<HttpTimelineInfo>;
 -  getTimeline(
 -    timelineName: string,
 -    query: {
 -      checkUniqueId?: string;
 -    }
 -  ): Promise<HttpTimelineInfo>;
 -  getTimeline(
 -    timelineName: string,
 -    query: {
 -      checkUniqueId?: string;
 -      ifModifiedSince: Date;
 -    }
 -  ): Promise<HttpTimelineInfo | NotModified>;
 -  async getTimeline(
 -    timelineName: string,
 -    query?: {
 -      checkUniqueId?: string;
 -      ifModifiedSince?: Date;
 -    }
 -  ): Promise<HttpTimelineInfo | NotModified> {
 -    await mockPrepare('timeline.get');
 -    try {
 -      const timeline = await getTimelineInfo(timelineName);
 -      if (query != null && query.ifModifiedSince != null) {
 -        if (timeline.lastModified >= query.ifModifiedSince) {
 -          return timeline;
 -        } else {
 -          if (
 -            query.checkUniqueId != null &&
 -            timeline.uniqueId != query.checkUniqueId
 -          ) {
 -            return timeline;
 -          } else {
 -            return new NotModified();
 -          }
 -        }
 -      }
 -
 -      return timeline;
 -    } catch (e) {
 -      if (
 -        e instanceof MockTimelineNotExistError ||
 -        e instanceof MockUserNotExistError
 -      ) {
 -        throw new HttpTimelineNotExistError();
 -      }
 -      throw e;
 -    }
 -  }
 -
 -  async postTimeline(
 -    req: HttpTimelinePostRequest,
 -    token: string
 -  ): Promise<HttpTimelineInfo> {
 -    await mockPrepare('timeline.post');
 -    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('timeline.patch');
 -    let modified = false;
 -    if (req.description != null) {
 -      modified = true;
 -      await setTimelinePropertyValue(
 -        timelineName,
 -        'description',
 -        req.description
 -      );
 -    }
 -    if (req.visibility != null) {
 -      modified = true;
 -      await setTimelinePropertyValue(
 -        timelineName,
 -        'visibility',
 -        req.visibility
 -      );
 -    }
 -    if (modified) {
 -      await updateTimelineLastModified(timelineName);
 -    }
 -    return await getTimelineInfo(timelineName);
 -  }
 -
 -  async deleteTimeline(timelineName: string, _token: string): Promise<void> {
 -    await mockPrepare('timeline.delete');
 -    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('timeline.member.put');
 -    const oldMembers =
 -      (await getTimelinePropertyValue<string[] | null>(
 -        timelineName,
 -        'members'
 -      )) ?? [];
 -    if (!oldMembers.includes(username)) {
 -      await setTimelinePropertyValue(timelineName, 'members', [
 -        ...oldMembers,
 -        username,
 -      ]);
 -      await updateTimelineLastModified(timelineName);
 -    }
 -  }
 -
 -  async memberDelete(
 -    timelineName: string,
 -    username: string,
 -    _token: string
 -  ): Promise<void> {
 -    await mockPrepare('timeline.member.delete');
 -    const oldMembers =
 -      (await getTimelinePropertyValue<string[] | null>(
 -        timelineName,
 -        'members'
 -      )) ?? [];
 -    if (oldMembers.includes(username)) {
 -      await setTimelinePropertyValue(
 -        timelineName,
 -        'members',
 -        without(oldMembers, username)
 -      );
 -      await updateTimelineLastModified(timelineName);
 -    }
 -  }
 -
 -  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('timeline.post.list');
 -    // 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('timeline.post.data.get');
 -    // 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('timeline.post.post');
 -    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('timeline.post.delete');
 -    // 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()
 -    );
 -  }
 -}
 +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" +  | "lastModified" +  | "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) +  ) as Promise<T>; +} + +function setTimelinePropertyValue<T>( +  name: string, +  property: TimelinePropertyKey, +  value: T +): Promise<void> { +  return mockStorage +    .setItem<T>(getTimelinePropertyKey(name, property), value) +    .then(); +} + +function updateTimelineLastModified(name: string): Promise<void> { +  return setTimelinePropertyValue( +    name, +    "lastModified", +    new Date().toISOString() +  ); +} + +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()); +      await updateTimelineLastModified(name); +    } +  } 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", +    lastModified: new Date( +      await getTimelinePropertyValue<string>(name, "lastModified") +    ), +    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); +  await updateTimelineLastModified(name); +} + +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) +  ) as Promise<T>; +} + +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("timeline.list"); +    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; +    }); +  } + +  getTimeline(timelineName: string): Promise<HttpTimelineInfo>; +  getTimeline( +    timelineName: string, +    query: { +      checkUniqueId?: string; +    } +  ): Promise<HttpTimelineInfo>; +  getTimeline( +    timelineName: string, +    query: { +      checkUniqueId?: string; +      ifModifiedSince: Date; +    } +  ): Promise<HttpTimelineInfo | NotModified>; +  async getTimeline( +    timelineName: string, +    query?: { +      checkUniqueId?: string; +      ifModifiedSince?: Date; +    } +  ): Promise<HttpTimelineInfo | NotModified> { +    await mockPrepare("timeline.get"); +    try { +      const timeline = await getTimelineInfo(timelineName); +      if (query != null && query.ifModifiedSince != null) { +        if (timeline.lastModified >= query.ifModifiedSince) { +          return timeline; +        } else { +          if ( +            query.checkUniqueId != null && +            timeline.uniqueId != query.checkUniqueId +          ) { +            return timeline; +          } else { +            return new NotModified(); +          } +        } +      } + +      return timeline; +    } catch (e) { +      if ( +        e instanceof MockTimelineNotExistError || +        e instanceof MockUserNotExistError +      ) { +        throw new HttpTimelineNotExistError(); +      } +      throw e; +    } +  } + +  async postTimeline( +    req: HttpTimelinePostRequest, +    token: string +  ): Promise<HttpTimelineInfo> { +    await mockPrepare("timeline.post"); +    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("timeline.patch"); +    let modified = false; +    if (req.description != null) { +      modified = true; +      await setTimelinePropertyValue( +        timelineName, +        "description", +        req.description +      ); +    } +    if (req.visibility != null) { +      modified = true; +      await setTimelinePropertyValue( +        timelineName, +        "visibility", +        req.visibility +      ); +    } +    if (modified) { +      await updateTimelineLastModified(timelineName); +    } +    return await getTimelineInfo(timelineName); +  } + +  async deleteTimeline(timelineName: string, _token: string): Promise<void> { +    await mockPrepare("timeline.delete"); +    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("timeline.member.put"); +    const oldMembers = +      (await getTimelinePropertyValue<string[] | null>( +        timelineName, +        "members" +      )) ?? []; +    if (!oldMembers.includes(username)) { +      await setTimelinePropertyValue(timelineName, "members", [ +        ...oldMembers, +        username, +      ]); +      await updateTimelineLastModified(timelineName); +    } +  } + +  async memberDelete( +    timelineName: string, +    username: string, +    _token: string +  ): Promise<void> { +    await mockPrepare("timeline.member.delete"); +    const oldMembers = +      (await getTimelinePropertyValue<string[] | null>( +        timelineName, +        "members" +      )) ?? []; +    if (oldMembers.includes(username)) { +      await setTimelinePropertyValue( +        timelineName, +        "members", +        without(oldMembers, username) +      ); +      await updateTimelineLastModified(timelineName); +    } +  } + +  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("timeline.post.list"); +    // 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("timeline.post.data.get"); +    // 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("timeline.post.post"); +    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("timeline.post.delete"); +    // 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 index 6929be2a..0a350894 100644 --- a/Timeline/ClientApp/src/app/http/mock/token.ts +++ b/Timeline/ClientApp/src/app/http/mock/token.ts @@ -1,53 +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('token.create');
 -    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('token.verify');
 -    try {
 -      const user = await getUser(checkToken(req.token));
 -      return {
 -        user,
 -      };
 -    } catch (e) {
 -      throw {
 -        isAxiosError: true,
 -        response: {
 -          status: 400,
 -        },
 -      } as Partial<AxiosError>;
 -    }
 -  }
 -}
 +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("token.create"); +    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("token.verify"); +    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 index 76a35f17..7948da11 100644 --- a/Timeline/ClientApp/src/app/http/mock/user.ts +++ b/Timeline/ClientApp/src/app/http/mock/user.ts @@ -1,140 +1,139 @@ -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
 -): asserts username is 'user' | 'admin' {
 -  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);
 -}
 -
 -const uniqueIdMap = {
 -  user: 'e4c80127d092d9b2fc19c5e04612d4c0',
 -  admin: '5640fa45435f9a55077b9f77c42a77bb',
 -};
 -
 -export async function getUser(
 -  username: 'user' | 'admin' | string
 -): Promise<HttpUser> {
 -  checkUsername(username);
 -  const savedNickname = await mockStorage.getItem<string>(
 -    `user.${username}.nickname`
 -  );
 -  return {
 -    uniqueId: uniqueIdMap[username],
 -    username: username,
 -    nickname:
 -      savedNickname == null || savedNickname === '' ? username : savedNickname,
 -    administrator: username === 'admin',
 -  };
 -}
 -
 -export class MockHttpUserClient implements IHttpUserClient {
 -  async get(username: string): Promise<HttpUser> {
 -    await mockPrepare('user.get');
 -    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('user.patch');
 -    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('user.avatar.get');
 -
 -    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('user.avatar.put');
 -    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('userop.changepassowrd');
 -    throw new Error('Not Implemented.');
 -  }
 -}
 +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 +): asserts username is "user" | "admin" { +  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); +} + +const uniqueIdMap = { +  user: "e4c80127d092d9b2fc19c5e04612d4c0", +  admin: "5640fa45435f9a55077b9f77c42a77bb", +}; + +export async function getUser( +  username: "user" | "admin" | string +): Promise<HttpUser> { +  checkUsername(username); +  const savedNickname = await mockStorage.getItem<string>( +    `user.${username}.nickname` +  ); +  return { +    uniqueId: uniqueIdMap[username], +    username: username, +    nickname: +      savedNickname == null || savedNickname === "" ? username : savedNickname, +    administrator: username === "admin", +  }; +} + +export class MockHttpUserClient implements IHttpUserClient { +  async get(username: string): Promise<HttpUser> { +    await mockPrepare("user.get"); +    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("user.patch"); +    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("user.avatar.get"); + +    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("user.avatar.put"); +    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("userop.changepassowrd"); +    throw new Error("Not Implemented."); +  } +} | 
