<file_sep>/*
 * MIT License
 *
 * Copyright (c) 2020 <EMAIL>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.engineer365.platform.user.app.service;

import org.engineer365.platform.user.api.bean.Account;
import org.engineer365.platform.user.api.bean.User;
import org.engineer365.platform.user.api.req.AccountAuthReq;
import org.engineer365.platform.user.api.req.CreateAccountByEmailReq;
import org.engineer365.platform.user.api.req.CreateUserByEmailReq;
import org.engineer365.platform.user.app.entity.AccountEO;
import org.engineer365.platform.user.app.entity.UserEO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import org.engineer365.common.misc.UuidHelper;
import org.engineer365.platform.user.api.UserApiV1;

@Service
@lombok.Setter
@lombok.Getter
@Transactional(propagation = Propagation.REQUIRED)
public class UserApiV1Service implements UserApiV1 {

    @Autowired
    AccountService accountService;

    @Autowired
    UserService userService;

    @Override
    public Account getAccount(String accountId) {
        var r = getAccountService().getAccount(false, accountId);
        return AccountEO.OUTPUT_COPIER.copy(r);
    }

    @Override
    public Account getAccountByEmail(String email) {
        var r = getAccountService().getAccountByEmail(false, email);
        return AccountEO.OUTPUT_COPIER.copy(r);
    }

    @Override
    public User getUser(String userId) {
        var r = getUserService().getUser(false, userId);
        return UserEO.OUTPUT_COPIER.copy(r);
    }

    @Override
    public String authByAccount(AccountAuthReq areq) {
        return getAccountService().authByAccount(areq);
    }

    @Override
    public Account createUserByEmail(CreateUserByEmailReq req) {
        var aidAndUid = UuidHelper.shortUuid();

        var ureq = CreateUserByEmailReq.USER_REQ_COPIER.copy(req);
        ureq.setPrimaryAccountId(aidAndUid);
        getUserService().createUser(aidAndUid, ureq);

        var areq = CreateUserByEmailReq.ACCOUNT_REQ_COPIER.copy(req);
        areq.setUserId(aidAndUid);
        return _createAccountByEmail(areq, aidAndUid);
    }

    @Override
    public Account createAccountByEmail(CreateAccountByEmailReq req) {
        var aid = UuidHelper.shortUuid();
        return _createAccountByEmail(req, aid);
    }

    Account _createAccountByEmail(CreateAccountByEmailReq req, String accountId) {
        var u = getUserService().getUser(true, req.getUserId());
        var a = getAccountService().createAccountByEmail(accountId, req, u);
        return AccountEO.OUTPUT_COPIER.copy(a);
    }

}
<file_sep>/*
 * MIT License
 *
 * Copyright (c) 2020 engineer365.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.engineer365.common;

import java.util.Set;


import org.engineer365.common.error.ForbiddenError;
import org.engineer365.common.misc.StringHelper;
import org.engineer365.common.security.Role;
import org.engineer365.common.bean.Dumpable;

/**
 * 当前API请求的上下文（主要是用户和权限等）
 */
@lombok.Getter
@lombok.Setter
public class RequestContext extends Dumpable {

  static final ThreadLocal<RequestContext> CURRENT = new ThreadLocal<>();

  String userId;

  Set<Role> roles;

  public static RequestContext bind(RequestContext ctx) {
    var old = current();
    CURRENT.set(ctx);
    LogTrace.setContext(ctx);

    return old;
  }

  public static RequestContext copy(RequestContext that) {
    var r = new RequestContext();

    if (that != null) {
      r.setUserId(that.getUserId());
      r.setRoles(that.getRoles());
    }

    return r;
  }

  public static void clear() {
    LogTrace.clear();
    CURRENT.remove();
  }

  public static RequestContext current() {
    return CURRENT.get();
  }

  public static RequestContext load(boolean ensureHasUserId) {
    var r = current();
    if (r == null) {
      throw new ForbiddenError(ForbiddenError.Code.CONTEXT_NOT_FOUND);
    }

    if (ensureHasUserId && StringHelper.isBlank(r.getUserId())) {
      throw new ForbiddenError(ForbiddenError.Code.USER_ID_NOT_FOUND_IN_CONTEXT);
    }

    return r;
  }

}
<file_sep>/*
 * MIT License
 *
 * Copyright (c) 2020 engineer<EMAIL>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.engineer365.platform.user.app.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import org.engineer365.platform.user.api.bean.Account;
import org.engineer365.platform.user.api.enums.AccountType;
import org.engineer365.platform.user.api.req.CreateAccountByEmailReq;


import org.engineer365.common.bean.BeanCopyer;
import org.engineer365.common.entity.UpdateableEO;

@lombok.Getter
@lombok.Setter
@lombok.NoArgsConstructor
@Entity
@Table(name = "user_account")
public class AccountEO extends UpdateableEO {

    public static final BeanCopyer<AccountEO, Account> OUTPUT_COPIER = new BeanCopyer<AccountEO, Account>(
            AccountEO.class, Account.class, Account::new, Account[]::new) {

        @Override
        protected void doRender(AccountEO source, Account target) {
            super.doRender(source, target);

            var u = source.getUser();
            if (u != null) {
                target.setUserId(u.getId());
            }
        }
    };

    public static final BeanCopyer<CreateAccountByEmailReq, AccountEO> CREATE_BY_EMAIL_REQ_COPIER
        = new BeanCopyer<CreateAccountByEmailReq, AccountEO>(
            CreateAccountByEmailReq.class, AccountEO.class, AccountEO::new, AccountEO[]::new) {

        @Override
        protected void doRender(CreateAccountByEmailReq source, AccountEO target) {
            super.doRender(source, target);

            target.setType(AccountType.EMAIL);
            target.setCredential(source.getEmail());
        }
    };

    @ManyToOne(optional = false)
    @JoinColumn(name = "user_id", nullable = false)
    UserEO user;

    @Enumerated(EnumType.STRING)
    @Column(length = 16, name = "type", nullable = false)
    AccountType type;

    @Column(length = 64, name = "credential", nullable = false)
    String credential;

    @Column(name = "password", length = 64, nullable = false)
    String password;

}
<file_sep>/*
 * MIT License
 *
 * Copyright (c) 2020 engineer<EMAIL>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.engineer365.common.service;

import org.engineer365.common.error.GenericError;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.mockito.ArgumentMatchers;
import org.mockito.MockitoAnnotations;
import org.opentest4j.AssertionFailedError;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.api.BeforeEach;

/**
 */
@Disabled
public abstract class ServiceTestBase {


  @BeforeEach
  public void beforeEach() {
    MockitoAnnotations.openMocks(this);
  }

  public static boolean matchBoolean(boolean that) {
    return ArgumentMatchers.booleanThat(b -> {
      Assertions.assertEquals(Boolean.valueOf(that), b);
      return true;
    });
  }

  public static String matchString(String that) {
    return ArgumentMatchers.argThat((String actual) -> that.equals(actual));
  }

  public static int matchInt(int that) {
    return ArgumentMatchers.intThat(i -> {
      Assertions.assertEquals(Integer.valueOf(that), i);
      return true;
    });
  }

  @SuppressWarnings("unchecked")
  public static <T extends GenericError> T assertThrows (Class<T> expectedType, Enum<?> code, Executable executable) {
      try {
        executable.execute();
      } catch (Throwable actualException) {
        if (expectedType.isInstance(actualException)) {
          return (T) actualException;
        }

        // UnrecoverableExceptions.rethrowIfUnrecoverable(actualException);

        throw new AssertionFailedError("caught unexpected exception", expectedType, actualException.getClass(), actualException);
      }

      throw new AssertionFailedError("no expected exception throw", expectedType, "null");
    }

}
<file_sep>/*
 * MIT License
 *
 * Copyright (c) 2020 <EMAIL>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.engineer365.common.json;

import com.fasterxml.jackson.core.type.TypeReference;

/**
 * Json工具类
 */
public class JSON {

  public static final ThreadLocal<JacksonHelper> MAPPER
    = ThreadLocal.withInitial(() -> new JacksonHelper(JacksonHelper.buildMapper()));

  /** 序列化成易读格式的JSON字符串 */
  public static String pretty(Object object) {
    return to(object, true);
  }

  /** 序列化成压缩格式的JSON字符串 */
  public static String to(Object object) {
    return to(object, false);
  }

  /**
   * 序列化成指定格式的JSON字符串
   *
   * @param object 待序列化成JSON字符串的对象
   * @param pretty true表示转成易读格式，否则转成压缩格式
   */
  public static String to(Object object, boolean pretty) {
    return MAPPER.get().to(object, pretty);
  }

  /**
   * 把JSON反序列化成指定类型的对象
   *
   * @param text  JSON字符串
   * @param clazz 指定类型
   */
  public static <T> T from(String json, Class<T> clazz) {
    return MAPPER.get().from(json, clazz);
  }


  /**
   * 把JSON反序列化成指定类型引用的对象，通常用于generic对象
   *
   * @param text  JSON字符串
   * @param clazz 指定类型引用
   */
  public static <T> T from(String json, TypeReference<T> typeReference) {
    return MAPPER.get().from(json, typeReference);
  }

}
<file_sep>--
-- MIT License
--
-- Copyright (c) 2020 <EMAIL>3<EMAIL>
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.

-- 用户模块的MySQL数据库初始化SQL

SET FOREIGN_KEY_CHECKS=0;

-- DROP TABLE IF EXISTS `user_account`;
CREATE TABLE `user_account` (
    `id`                 CHAR(22)     CHARACTER SET latin1   NOT NULL,
    `created_at`         DATETIME(3)                         NOT NULL,
    `version`            INT                                 NOT NULL,
    `modified_at`        DATETIME(3)                         NOT NULL,

    `user_id`            CHAR(22)     CHARACTER SET latin1   NOT NULL,

    `type`               VARCHAR(16)  CHARACTER SET latin1   NOT NULL,
    `credential`         VARCHAR(64)   NOT NULL,

    `password`           VARCHAR(64)  CHARACTER SET latin1   NOT NULL,

    PRIMARY KEY (`id`)
) ENGINE=InnoDB, DEFAULT CHARACTER SET utf8mb4;

CREATE INDEX `idx_user_account_user_id` ON `user_account`(`user_id`(8));
CREATE UNIQUE INDEX `idx_user_account_biz_key` ON `user_account`(`credential`, `type`);


-- DROP TABLE IF EXISTS `user_user`;
CREATE TABLE `user_user` (
    `id`                 CHAR(22)     CHARACTER SET latin1   NOT NULL,
    `created_at`         DATETIME(3)                         NOT NULL,
    `version`            INT                                 NOT NULL,
    `modified_at`        DATETIME(3)                         NOT NULL,

    `name`               VARCHAR(32)   NOT NULL,
    `full_name`          VARCHAR(64)   NOT NULL,
    `primary_account_id` CHAR(22),
    `is_root`            BOOLEAN DEFAULT FALSE               NOT NULL,

    PRIMARY KEY (`id`)
) ENGINE=InnoDB, DEFAULT CHARACTER SET utf8mb4;

CREATE UNIQUE INDEX `idx_user_user_name` ON `user_user`(`name`);

<file_sep>#!/bin/bash

# 
#  MIT License
# 
#  Copyright (c) 2020 <EMAIL>
# 
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
# 
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
# 
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#  SOFTWARE.

set -e

work_dir=$dev_dir/work
mysql_dir=$work_dir/mysql

# 'Darwin' or 'Linux'
export OS=`uname -s`
case "$OS" in
  "Darwin" )
  _sudo=''
  ;;
  "Linux" )
  _sudo='sudo'
  # sudo chown -R $USER:$USER $data_dir
  ;;
  * )
    echo "Support MAC-OS-X or Linux only"
    exit 1
  ;;
esac

# print the environment
echo ''
echo 'Work directory: ' $work_dir
echo 'MySQL data directory: ' $mysql_dir
echo ''
<file_sep># 一个示范性项目：云原生的微服务开发工程

  （项目刚启动，组队中，欢迎加入）

## 两个出发点

   - 示范完整的云原生的微服务架构

     已有的开源项目，Spring Cloud系列貌似更多，云原生的微服务架构其实也是一个主流路线，会成为甚至已经成为下一代新的技术。种种原因，在国内的普及程度相对滞后，值得我们去做些什么。

   - 整个开发过程以接近实际工程的方式展示出来，包括但不限于：持续集成、持续部署、自动化运维、多开发环境管理，等等

     我做了调研，总结下来是，有的专注于某一技术点，有的专注于小范围内的整合，有的专注于完整的产品级代码，还有的专注于教学式的课程。代码和工程的系统结合目前我没看到，但价值的分量明显足够。

## 具体想法：
   - 采用云原生的微服务架构: Docker, Kubernetes, ServiceMesh。
   - 实现一个功能需求上能覆盖计划的技术栈的Web应用，目前考虑是一个二手计算机图书电商系统，注意，是<b>真的准备让大家的书上架的</b>！
   - 前后端分离，以后端为主，后端最初会以JAVA的Spring Boot全家桶开始，包括MySQL、Redis、RabbitMQ、ElasticSearch等基本的主流技术栈，然后一步步增加别的，包括支持GO等其他语言；前端最初用React或Vue的全家桶开发SPA Web应用，然后扩展到小程序和手机App，逐步建立大前端体系。
   - 自动化测试：单元测试、集成测试、性能测试、压力测试、端对端功能测试。整体测试覆盖率达到100%。
   - 以DevOps和GitOps为理念的工程开发体系：
     - 持续集成（CI），持续部署（CD）
     - 自动化运维、自动化监控报警、集中式日志、分布式调用链路追踪
     - 部署私有仓库
     - 整合常见开发工具，譬如SonarQube, Slack，GITHUB Issue、GITHUB Project等。
     - 建立示范性的本机开发环境、测试环境、预生产环境和线上生产环境
   - 所使用的每一项技术都建立指南文档。
   - 后续会加入大数据分析。
   - 考虑演进成Serverless/FaaS，目前还没定，也可能会是另一个新项目。

## 如何报名参加？

  这个项目是开放的，开源项目理论上没有门槛，谁想参加都可以，PR是最硬核的对参与程度的衡量，PR一合并会自动记入贡献者名单。

  我已经根据一期开发的目标动手在写代码了。我相信大家肯定是很感兴趣的，但必然有很多人会因为时间精力等各种原因还没法直接参与开发。其实做贡献的方式有很多种，不是只有PR才算是参与了，很欢迎大家能提出建议、提出需求、帮助测试提BUG、帮助写文档，等等。所以，只要你有一定兴趣，那么：

  1. 请Fork这个项目，把你的GITHUB账号名加到[./members.md](./members.md)那个文件，然后提交PR，等待合并。

  2. Watch这个项目，任何项目变动都能得到邮件通知。

  3. PR合并成功后，加入QQ群1033056382（目前不打算用微信）。加群时需要回答你提交PR时用的GITHUB账号。

  4. 入群后，请把你的群内昵称改为GITHUB账号名，否则会被踢出。

  5. 可选：Star一下，有助于我们找到更多参与者

  对这个过程如果有什么问题，可以提交Issue [https://github.com/engineer-365/cloud-native-micro-service-engineering/issues]

## 目标：

   （欢迎补充等建议）

  - 一期开发：
    - 阶段性目标：
      - 最小化功能实现，前后端打通，但只实现书店顾客前端，不做管理员后端
      - 模块化的单体应用
      - 手动部署，直接上线运行
    - 阶段性技术栈：
      - 后端 -> Java技术栈:
         - [ ] Spring MVC: REST API(JSON), Swagger (Spring Fox)
         - [ ] 测试: Junit5 (单元测试), Mockito (Mock), JaCoCo (测试覆盖率分析)
         - [ ] 数据库访问：Spring Data JPA（Hibernate），QueryDSL（动态查询），Flyway（数据库升级管理）
         - [ ] 日志：Logback
         - [ ] Jar包依赖管理：Maven
         - [ ] Spring Boot Actuator：监控数据采集和管理
         - [ ] 其他：Guava（工具类），Lombok (Java Bean代码生成)
      - 前端 -> React全家桶：
         - [ ] Webpack
         - [ ] 路由：React Router
         - [ ] UI库：Material Design或Ant Design
         - [ ] REST客户端：Axios
         - [ ] 测试：Jest或Mocha
      - [ ] Nginx做前端静态页面和后端API的统一入口
      - [ ] 关系数据库：MySQL单节点，H2（测试用）
      - [ ] 持续集成（CI）：Jenkins, SonarQube (代码质量检查)
      - [ ] Maven私有仓库：Nexus，或JFrog
      - [ ] 私有Docker仓库：Habor
      - [ ] 开发者本地Docker开发环境：Docker-compose
      - [ ] 用Helm或Kustomize手动部署到Kubernetes
      - [ ] Spring Boot Admin
  <img src="./doc/phases/phase-1.png" alt="image"/>

  - 二期开发：
    - 阶段性目标：
      - 主要功能全部实现，包括管理员前端
      - 初步实现云原生的微服务架构
      - 自动部署
    - 阶段性技术栈
      - 后端增加：
        - [ ] 参数校验: javax.validation (JSR-303, Hibernate Validator)
        - [ ] 测试: Cucumber, TestContainers (容器化测试)，JMeter (性能测试和压测)，Contract
        - [ ] 权限控制：Spring Security (OAuth + JWT)
        - [ ] 日志：JSON日志格式
        - [ ] REST客户端: Resilience4J，Spring RestTemplate或Open Feign
        - [ ] 缓存：Jedis, Spring Data Redis, Caffeine
        - [ ] 全文检索：Spring Data Elasticsearch
        - [ ] 消息队列：Spring AMQP, RabbitMQ
        - [ ] 定时任务：TBD
      - 前端增加：
        - [ ] 状态管理：Redux或MobX
        - [ ] 测试：Selenium，Puppeteer
    - [ ] 关系数据库：高可用MySQL集群（MySQL Router + Group Replication + MySQL Shell）
    - [ ] 持续部署（CD）：Ranchor(Kubernetes集群管理），gitkube, ArgoCD（自动部署到Kubernetes）
    - [ ] 分布式缓存：Redis, Redis Sentinel
    - [ ] ELK (Elasticsearch + Logstash + Kibana): 全文检索，日志管理
    - [ ] 配置集中管理：Consul，Vault（MySQL等密码管理），Git2Consul
    - [ ] 分布式调用链路追踪：OpenTracing, Jaeger
    - [ ] 监控报警：Prometheus，Grafana
    - [ ] Skaffold
  <img src="./doc/phases/phase-2.png" alt="image"/>

  - 三期开发：
    - 阶段性目标：
      - 主要功能全部实现
      - 完整实现云原生的微服务架构
      - 支持多语言
      - 云原生的持续集成、持续部署、自动化运维
    - 阶段性技术栈
      - 后端：
        - [ ] 支持用GO，Python，Node.JS，PHP开发微服务
        - [ ] 部分微服务改用Serverless/FaaS实现
        - [ ] 部分微服务改用异步/WebFlux实现
        - [ ] GraphQL API
        - [ ] gRPC
        - [ ] WebSocket
        - [ ] Workflow
      - 前端：
        - [ ] GraphQL API
        - [ ] snowplow
      - [ ] API网关：Kong，Ambassador，Gloo或Traefix？
      - [ ] ServiceMesh: lstio
      - [ ] NoSQL数据库：MongoDB, Neo4J
      - [ ] 自动化运维：Ansible，Vagrant，Terraform
      - [ ] Jenkins-X
  <img src="./doc/phases/phase-3.png" alt="image"/>

  - 四期开发：
    - 阶段性目标：
      - 对接云供应商：阿里云，AWS，Azure等
      - 大数据分析：Flink，Hive, Kafka, ClickHouse等
      - 全面高可用
      - 尝试更多新技术
      - 手机App、微信小程序
    - 阶段性技术栈
      - GraalVM, Quarkus
      - Flutter
      - Crossplane
      - chaos-mesh
      - TiDB, PostgreSQL/Cockroachdb
  <img src="./doc/phases/phase-4.png" alt="image"/>

## 问答：

> 1. "太多了，一看就跟不上，就连这个简介都看不懂"

  首先，我的观点是：看得懂就不用一起学了。很多我也只是个大概的了解，一步一步来吧，让我们在实践中一起学习。现在的趋势就是云原生、微服务、DevOps，列出的这些技术是在跟着大趋势走，避免走弯路，避免重复发明轮子，希望走到主流的前排里。跟着教程书学效果不好，真正的技术只可能在系统化的实践中才学得到学得好。

  然后，太多的技术点（实际上还只是一小部分）恰恰也是为什么要发起这个项目的原因之一：想示范如何系统的使用这些技术。列出这么多名词术语有些唬人，因为大部分其实是成熟公司里已经用起来的技术，但是对刚入门的程序员来说就是个门槛，会懵，用过了、过了这个门槛，会发现很多事情只是知道不知道的区别。

 > 2. “为什么不是Spring Cloud？”

  云原生是微服务架构的主流路线之一，特点是服务治理的实现需要代码修改量少甚至不需要修改，意味着代码侵入性小，多语言支持方便，扩展性强。

  另一方面，Spring Cloud的优秀开源演示项目已经很多，相对来说云原生方案因为比较新，项目发展空间更大，而且，云原生架构对基础架构和工程规范化的要求更高，而演示基础架构和规范化的工程开发正是这个项目的主要目标之一，希望能通过这个项目为促进云原生方案的普及作出贡献。

  另外，项目里会用到一些Spring Cloud体系内的组件，但总体来说，并不是Spring Cloud的方案。

> 3. “我不喜欢或者不习惯你提到的技术，我还想尝试一个更新的技术，能不能换成xx？”，譬如，有群友提到：“最近正好有计划做一个类似的，想做的平台是基于vertex的kotlin和actix的rust，以及尽量全套的异步组件，顺便试试postgresql。”

  选择成熟的主流技术是刻意的，这个项目的主要目标不是调研和尝鲜，主要目标是让大家脱离教程和作坊式的开发，让大家有机会实践贴近真实的规范化的工程开发。涉及的方方面面的细节会很多很杂，所以，为了降低项目失败的可能性，至少在前两阶段的计划中，我刻意采取了保守的技术选择策略。

  这和尝试更新的技术并不矛盾，在初期的迭代取得了阶段性成果以后，我们会得到一个很好的平台，我们会更有士气更有真实需求去实践更多的东西，甚至做些重复发明轮子的事情都可以。

  所以，就拿上面这个问题提到的“异步/非阻塞”的这个话题来说，因为在典型的Java Web应用系统里异步还不是主流，所以虽然这个想法不错，可是不太适合初始阶段，后续肯定会尝试异步方案。

> 4. “一开始关注后端就好了，也不仅仅限制于前后端分离，需要的是提供如何将自己的前端技术栈接入该后端项目的接口规范和指南，像react，vue，或者后端渲染，或者模板引擎等等”

  第一期计划里是安排了实现一个书店顾客的Web前端，因为定位是一个系统化的工程化的演示性项目，前后端一起联动着实践才能体现工程中一个接口的开发过程。

> 5. "可以提供一些项目额外的包，例如该项目叫awesome, 就可以提供awesome-react-starter, awesome-vue-starter, awesome-blazor-starter, 毕竟专注后端，在一个项目集成与前端相关的一些东西多少会有些不好"

  是的，会产出一些框架性的胶水性的代码和工具，会控制在很薄的一层封装上，目的是减少重复代码，提高工程效率和架构细节上的的一致性。顺便强调一下，这个项目并不是专注后端，而是因为通常产品开发围绕着后端作为中心，所以目前重点演示以后端为主的完整产品开发工程。


<file_sep>/*
 * MIT License
 *
 * Copyright (c) 2020 <EMAIL>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.engineer365.platform.user.app.service;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import static org.mockito.ArgumentMatchers.argThat;

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.engineer365.platform.user.api.req.CreateUserReq;
import org.engineer365.platform.user.app.dao.UserDAO;
import org.engineer365.platform.user.app.entity.UserEO;

import org.engineer365.common.service.ServiceTestBase;

public class UserServiceTest extends ServiceTestBase {

    @Mock
    UserDAO dao;

    @InjectMocks
    UserService target;

    @Test
    void test_createAccount_happy() {
        var userId = "u-1";

        var req = new CreateUserReq();
        req.setFullName("n-1");

        when(this.dao.save(ArgumentMatchers.any())).thenReturn(new UserEO());
        this.target.createUser(userId, req);

        verify(this.dao).save(argThat((UserEO entity) -> {
            Assertions.assertNotNull(entity.getId());
            Assertions.assertEquals(req.getFullName(), entity.getFullName());
            return true;
        }));
    }

}
<file_sep>/*
 * MIT License
 *
 * Copyright (c) 2020 <EMAIL>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.engineer365.platform.user.app.service;

import org.engineer365.common.error.BadRequestError;
import org.engineer365.platform.user.api.enums.ErrorCode;
import org.engineer365.platform.user.api.req.CreateUserReq;
import org.engineer365.platform.user.app.entity.UserEO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.engineer365.platform.user.app.dao.UserDAO;

@Service
@lombok.Setter
@lombok.Getter
public class UserService {

    @Autowired
    UserDAO dao;

    public UserEO getUser(boolean ensureExists, String userId) {
        return getDao().get(ensureExists, userId);
    }

    public UserEO getByName(String name) {
        return getDao().getByName(name);
    }

    public UserEO createUser(String userId, CreateUserReq req) {
        var d = getDao();
        if (d.getByName(req.getName()) != null) {
            throw new BadRequestError(ErrorCode.USER_NAME_DUPLICATES);
        }

        var r = UserEO.CREATE_REQ_COPIER.copy(req);
        r.setId(userId);

        return d.save(r);
    }

}
<file_sep>开发记录：

2020-11-28:
  - [./server_code_init.md](./server_code_init.md): 后端代码始化
<file_sep>## 后端代码初始化：

- 使用spring boot的官方工具: https://start.spring.io

  这个是生成的记录：https://start.spring.io/#!type=maven-project&language=java&platformVersion=2.4.0.RELEASE&packaging=jar&jvmVersion=11&groupId=org.engineer365&artifactId=fleashop&name=fleashop&description=Demo%20project%20for%20Spring%20Boot&packageName=org.engineer365.fleashop&dependencies=lombok,configuration-processor,web,data-jpa,flyway,h2,mysql,actuator

  截图：[./server_code_init/start.spring.io.png](./init/start.spring.io.png)

  start.spring.io生成的[./server_code_init/HELP.md](./server_code_init/HELP.md):<file_sep># 二手书店的服务器端

## 示范的技术

  - Spring Boot:
    - 官网：https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/

  - JaCoCo:
    测试覆盖率工具
    - 官方文档： https://www.jacoco.org/jacoco/trunk/doc/

  - Flyway:
    数据库初始化和升级管理
    - 官网：https://flywaydb.org/

  - QueryDSL:
    替代JPA自身的Criteria API，和Criteria API一样支持灵活的动态数据库查询语句，但相比JPA Criteria API：
    1. 通过插件把数据库表的表名和列名绑定到类型安全的Java Bean property，方便重构，也减少直接使用字符串时容易发生的拼写错误
    2. 查询条件用DSL风格的级联方式表达，接近直接写SQL，简单直观，容易学习使用

    - 官网：http://www.querydsl.com/
    - 简介文章：https://www.baeldung.com/intro-to-querydsl
    - 参考手册：http://www.querydsl.com/static/querydsl/latest/reference/html_single/

  - H2:
    仅用于单元测试。
    - 用于单元测试的优点：
      1. 快 - 对于单元测试这非常重要
      2. 不需要数据清理 - 因为H2是内存数据库，默认设置下数据不放磁盘，因此简化了单元测试的准备工作

  - Lombok:
    代码生成插件，通过简单的annotation来消除一些常见的无聊的Java代码，譬如Getter和Setter。

    - 官网：https://projectlombok.org
    - 简介文章：
      1. https://blog.csdn.net/ThinkWon/article/details/101392808
      2. https://www.jianshu.com/p/365ea41b3573

    注意：IDE里使用需要安装和该IDE集成的插件，否则IDE会不会触发代码生成、找不到生成的代码而报编译错误。
    常见的IDE插件：
      1. VS Code：https://marketplace.visualstudio.com/items?itemName=GabrielBB.vscode-lombok
      2. IDEA：待补充
      3. Eclipse：待补充


## 本地构建

  - 编译打包：
     ```./mvnw package```
     第一次构建会从maven官网下载依赖到的很多第三方包，会很慢，最好是开着梯子。
     后续会搭建自己的代理/镜像服务器，因为代理/镜像服务器也是实际工程（国内）的一部分

  -  可选：下载第三方包的源代码和javadoc，方便调试和学习
     ```mvn dependency:sources```
     ```mvn dependency:resolve -Dclassifier=javadoc```
     
  - 启动/查看log／停止
    - Mac或Linux
      - ```dev/up.sh```
      - ```dev/log.sh```
      - ```dev/down.sh```
    - Windows
      - TODO：bat脚本待写，以下命令待验证
        ```shell
        cd dev
        docker-compose up --build -d --remove-orphans
        ```
        ```shell
        cd dev
        docker-compose logs
        ```
        ```shell
        cd dev
        docker-compose down --remove-orphans
        ```
   
  - 简单验证RESTful API：
    - VSCode的REST插件: 见[./dev/manual-test.rest](./dev/manual-test.rest)
    - Curl: ```curl -v --request GET --header 'content-type: application/json' --url http://localhost:28080/platform/user/api/v1/rest/user/_/xxx```
<file_sep>/*
 * MIT License
 *
 * Copyright (c) 2020 engineer<EMAIL>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.engineer365.platform.user.app.service;

import org.engineer365.platform.user.api.enums.AccountType;
import org.engineer365.platform.user.api.enums.ErrorCode;
import org.engineer365.platform.user.api.req.AccountAuthReq;
import org.engineer365.platform.user.api.req.CreateAccountByEmailReq;
import org.engineer365.platform.user.app.entity.AccountEO;
import org.engineer365.platform.user.app.entity.UserEO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.engineer365.common.error.BadRequestError;
import org.engineer365.common.error.NotFoundError;
import org.engineer365.platform.user.app.dao.AccountDAO;

@Service
@lombok.Setter
@lombok.Getter
public class AccountService {

    @Autowired
    AccountDAO accountDao;

    public AccountEO getAccount(boolean ensureExists, String accountId) {
        return getAccountDao().get(ensureExists, accountId);
    }

    public AccountEO getAccountByEmail(boolean ensureExists, String email) {
        var r = getAccountDao().getByCredentialAndType(email, AccountType.EMAIL);
        if (r == null && ensureExists) {
            throw new NotFoundError(ErrorCode.NO_ACCOUNT_WITH_SPECIFIED_EMAIL);
        }
        return r;
    }

    public void checkRequestWithAccount(AccountEO account, AccountAuthReq req) {
        if (account == null) {
            throw new NotFoundError(ErrorCode.ACCOUNT_NOT_FOUND);
        }

        if (account.getPassword().equals(req.getPassword()) == false) {
            throw new BadRequestError(ErrorCode.WRONG_PASSWORD);
        }
    }

    public String authByAccount(AccountAuthReq req) {
        var a = getAccount(false, req.getAccountId());
        checkRequestWithAccount(a, req);
        return a.getId(); //TODO: replace it with JWT
    }

    public AccountEO createAccountByEmail(String accountId, CreateAccountByEmailReq req, UserEO user) {
        if (getAccountDao().getByCredentialAndType(req.getEmail(), AccountType.EMAIL) != null) {
            throw new BadRequestError(ErrorCode.ACCOUNT_EMAIL_DUPLICATES);
        }

        var r = AccountEO.CREATE_BY_EMAIL_REQ_COPIER.copy(req); {
            r.setId(accountId);
            r.setUser(user);
        }

        return getAccountDao().save(r);
    }

}
<file_sep>
1. 请Fork这个项目，把你的GITHUB账号名加到[./members.md](./members.md)那个文件，然后提交PR，等待合并。
  
2. Watch这个项目，任何项目变动都能得到邮件通知。

3. PR合并成功后，加入QQ群1033056382（目前不打算用微信）。加群时需要回答你提交PR时用的GITHUB账号。
 
4. 入群后，请把你的群内昵称改为GITHUB账号名，否则会被踢出。
  
5. 可选：Star一下，有助于我们找到更多参与者

   QQ群：1033056382（目前不打算用微信）

   对这个过程如果有什么问题，可以提交Issue [https://github.com/engineer-365/cloud-native-micro-service-engineering/issues]

    | #     | GITHUB账号（必填）                     | 备注（可选）                    |
    | :---: | :----------------------------------- | :---------------------------: |
    |  1    | qiangyt                              | 发起人                         |
    |  2    | WeiLoongMao                          |                               |
    |  3    | CNYuYang                             |                               |
    |  4    | Jason_liu                            |                               |
    |  5    | zlmkenan001                          |                               |
    |  6    | knoxnoe                              |                               |
    |  7    | wqjzzgci                             |                               |
    |  8    | greedynamic                          |                               |
<file_sep>/*
 * MIT License
 *
 * Copyright (c) 2020 <EMAIL>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.engineer365.platform.user.app.service;

import org.engineer365.platform.user.api.enums.AccountType;
import org.engineer365.platform.user.api.enums.ErrorCode;
import org.engineer365.platform.user.api.req.AccountAuthReq;
import org.engineer365.platform.user.api.req.CreateAccountByEmailReq;
import org.engineer365.platform.user.app.dao.AccountDAO;
import org.engineer365.platform.user.app.entity.AccountEO;
import org.engineer365.platform.user.app.entity.UserEO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.engineer365.common.error.BadRequestError;
import org.engineer365.common.error.NotFoundError;
import org.engineer365.common.service.ServiceTestBase;


public class AccountServiceTest extends ServiceTestBase {

    @Mock
    AccountDAO accountDao;

    @InjectMocks
    AccountService target;

    @Test
    void test_checkRequestWithAccount_ACCOUNT_NOT_FOUND() {
        assertThrows (NotFoundError.class, ErrorCode.ACCOUNT_NOT_FOUND,
            () -> this.target.checkRequestWithAccount(null, new AccountAuthReq()));
        assertThrows (NotFoundError.class, ErrorCode.ACCOUNT_NOT_FOUND,
            () -> this.target.checkRequestWithAccount(null, null));
    }

    @Test
    void test_checkRequestWithAccount_PASSWORD_NOT_MATCHES() {
        var a = new AccountEO();
        a.setPassword("abc");

        var req = new AccountAuthReq();
        req.setPassword("def");

        assertThrows(BadRequestError.class, ErrorCode.WRONG_PASSWORD,
            () -> this.target.checkRequestWithAccount(a, req));
    }

    @Test
    void test_checkRequestWithAccount_OK() {
        var a = new AccountEO();
        a.setPassword("abc");

        var req = new AccountAuthReq();
        req.setPassword("abc");

        this.target.checkRequestWithAccount(a, req);
    }

    @Test
    void test_auth_accountNotFound() {
        var req = new AccountAuthReq();
        req.setAccountId("a-1");
        req.setPassword("p");

        when(this.accountDao.get(false, "a-1")).thenReturn(null);

        assertThrows(NotFoundError.class, ErrorCode.ACCOUNT_NOT_FOUND,
            () -> this.target.authByAccount(req));
    }

    @Test
    void test_auth_ok() {
        var req = new AccountAuthReq();
        req.setAccountId("a-1");
        req.setPassword("p");

        var a = new AccountEO();
        a.setId("a-1");
        a.setPassword("p");
        a.setCredential("<EMAIL>");
        a.setType(AccountType.EMAIL);

        when(this.accountDao.get(false, "a-1")).thenReturn(a);

        var r = this.target.authByAccount(req);
        Assertions.assertEquals(a.getId(), r);
    }

    @Test
    void test_createAccountByEmail_happy() {
        var accountId = "a-1";
        var user = new UserEO();
        user.setId("u-1");

        var req = new CreateAccountByEmailReq();
        req.setPassword("p");
        req.setUserId(user.getId());
        req.setEmail("<EMAIL>");

        when(this.accountDao.save(ArgumentMatchers.any())).thenReturn(new AccountEO());
        //when(this.emailSender.send(ArgumentMatchers.any())).thenReturn(null);

        this.target.createAccountByEmail(accountId, req, user);

        verify(this.accountDao).save(argThat((AccountEO entity) -> {
            Assertions.assertNotNull(entity.getId());
            Assertions.assertEquals(req.getPassword(), entity.getPassword());
            Assertions.assertSame(user, entity.getUser());
            return true;
        }));
    }

}
