14 - 로그인 프로세서 추가

오늘의 강좌는 로그인 기능을 실현하는 두 부분 중 첫 번째 부분이 될 것이다.우리는 우리가 사용자를 등록할 때 사용하는 많은 동일한 모델을 따를 것이며, 이 과정에서 새로운 방법을 실현할 것이다.
그림에서 보듯이 우리는 handler층 로그인 방법부터 시작하여 다음에 모든 다른 층의 상세한 정보를 갱신할 것이다. 그 중 일부 기능은 우리가 이미 실현했다!

여느 때와 마찬가지로 본 강좌의 모든 코드를 사용하여 서명repo on Github하고 각 과목의 한 부분을 포함합니다!
만약 당신이 동영상을 좋아한다면, 아래의 동영상 버전을 보십시오!

사용자 서비스 인터페이스에 로그인 추가


사용자가 로그인할 때, 우리는 사용자가 등록할 때와 같이 사용자의 전자메일과 비밀번호를 받아들이기를 희망합니다.로그인과 등록의 실현 세부 사항은 다르지만 방법 서명은 같다.SigninUserService 인터페이스에 ~/model/interfaces.go를 추가합니다.Signin와 마찬가지로, 우리는 부분적으로 채워진 *model.User을 전달하고, 로그인에 실패하면 오류를 되돌려줍니다.로그인에 성공하면 이 방법에 전달된 사용자는 모든 사용자의 상세한 정보를 포함하는 것으로 수정됩니다.
// UserService defines methods the handler layer expects
// any service it interacts with to implement
type UserService interface {
    Get(ctx context.Context, uid uuid.UUID) (*User, error)
    Signup(ctx context.Context, u *User) error
    Signin(ctx context.Context, u *User) error
}
이 업데이트는 DellmockUserService과 서비스 층UserService을 파괴할 것입니다. 왜냐하면 이 방법이 실현되지 않았기 때문입니다.Signup~/service/user_service.go의 불완전한 실현을 추가하면 다음 강좌에서 완성할 것입니다.
// Signin reaches our to a UserRepository check if the user exists
// and then compares the supplied password with the provided password
// if a valid email/password combo is provided, u will hold all
// available user fields
func (s *userService) Signin(ctx context.Context, u *model.User) error {
    panic("Not implemented")
}

아날로그 서명 방법 추가


프로세서 논리와 단원 테스트를 추가하기 전에 ~/model/mocks/user_service.go 방법으로 저희Signin를 업데이트하겠습니다.더 많은 정보는 이전의 테스트와 테스트 강좌testify를 참조하세요!
func (m *MockUserService) Signin(ctx context.Context, u *model.User) error {
    ret := m.Called(ctx, u)

    var r0 error
    if ret.Get(0) != nil {
        r0 = ret.Get(0).(error)
    }

    return r0
}

프로세서 추가


우리는 이 처리 프로그램에 대량의 코드를 추가할 것이기 때문에 Signin 처리 프로그램을 ~/handler/handler.go에서 자신의 파일~/handler/signin.go로 복사할 것이다.이 방법의 내용도 다음과 같이 업데이트할 것입니다.
package handler

// IMPORTS OMITTED

// signinReq is not exported
type signinReq struct {
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,gte=6,lte=30"`
}

// Signin used to authenticate extant user
func (h *Handler) Signin(c *gin.Context) {
    var req signinReq

    if ok := bindData(c, &req); !ok {
        return
    }

    u := &model.User{
        Email:    req.Email,
        Password: req.Password,
    }

    ctx := c.Request.Context()
    err := h.UserService.Signin(ctx, u)

    if err != nil {
        log.Printf("Failed to sign in user: %v\n", err.Error())
        c.JSON(apperrors.Status(err), gin.H{
            "error": err,
        })
        return
    }

    tokens, err := h.TokenService.NewPairFromUser(ctx, u, "")

    if err != nil {
        log.Printf("Failed to create tokens for user: %v\n", err.Error())

        c.JSON(apperrors.Status(err), gin.H{
            "error": err,
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "tokens": tokens,
    })
}
우리는 이 처리 프로그램 방법에서 다음과 같은 조작을 실행한다.
  • 우리가 전에 만든 helpersigninReq 함수를 사용하여 데이터를 bind_data에 연결합니다.데이터가 바인딩되지 않거나 검증에 실패하면 이 함수는 오류 요청 HTTP status 400 오류를 보냅니다.실제로 bind_data를 위해 단독 테스트를 만드는 것은 현명하다(사실이 증명하듯이 본 강좌에 결함이 존재한다)😂).
  • 유효한 데이터가 있다면 요청한 전자 우편과 비밀번호 필드에 부분적으로 채워진 *model.User를 만들 것입니다.
  • 사용 c.Request.Context()gin 상하문에서 요청 상하문을 추출합니다.
  • 접촉 UserService.Signin 방법 및 오류 처리만약 오류가 발생하면, 우리는 apperrors.Status() 함수를 사용하여 이 오류가 사용자 정의 apperrors 에 정의된 오류에 속하는지 확인합니다.
  • 사용자가 로그인에 성공하면 우리는 등록할 때와 같이 새로운 영패 쌍을 보낼 것입니다.우리는 또 어떤 잘못도 처리할 것을 확보한다.
  • 테스트 신호


    이 테스트를 위한 새 파일 ~/handler/signin_test.go 을 만듭니다.이 파일에서 우리는 다음과 같은 내용을 테스트할 것이다.
  • 잘못된 요청 데이터 - 모든 검증 오류 조합을 테스트하지는 않지만 오류 요청에 대한 정확한 오류 JSON 응답을 받을 수 있도록 사례를 테스트합니다.
  • UserService에서 반환된 오류입니다.서명
  • 성공적으로 호출TokenService.NewPairFromUser(이것은 처리 프로그램 결과가 성공했다는 것을 의미한다).
  • 의 오류입니다.마찬가지로, 우리는 처리 프로그램이 이런 상황에서 정확한 HTTP 응답을 보낼 수 있도록 확보하고 싶을 뿐이다.
  • 바디 및 설정 테스트


    우리는 테스트의 설정 부분 (TokenService.NewPairFromUser 블록 앞에서 우리의 아날로그 서비스,gin 엔진/공유기, 처리 프로그램을 실례화할 것입니다.
    그러나 설정에서 우리의 아날로그 방법 응답을 정의하는 것이 아니라 단일 t.Run 블록에서 mock.On(...) 으로 정의하기로 결정했습니다.내가 이렇게 한 것은 최종적으로 설정에서 정의와 변수를 호출하는 시뮬레이션 방법을 너무 많이 만들어서 어떤 변수가 어떤 테스트 용례에 대응하는지 이해하기 어려웠기 때문이다.
    package handler
    
    // IMPORTS OMITTED
    
    func TestSignin(t *testing.T) {
        // Setup
        gin.SetMode(gin.TestMode)
    
        // setup mock services, gin engine/router, handler layer
        mockUserService := new(mocks.MockUserService)
        mockTokenService := new(mocks.MockTokenService)
    
        router := gin.Default()
    
        NewHandler(&Config{
            R:            router,
            UserService:  mockUserService,
            TokenService: mockTokenService,
      })
    
      // Tests will be added here below
      // ...
    }
    

    잘못된 요청 데이터 사례


    이 예에서는 잘못된 전자 우편 주소가 있는 JSON 요청 주체를 만들고 이를 저희 프로세서에 보냈습니다. 테스트 설정 부분에서 이 프로세서를 실례화했습니다.우리는 HTTP 상태 400t.Run을 보낼 것이며 http.StatusBadRequestmockUserService.Signin 방법을 호출하지 않을 것이라고 단언했다. 왜냐하면 처리 프로그램은 이 방법에 도달하기 전에 되돌아와야 하기 때문이다.
        t.Run("Bad request data", func(t *testing.T) {
            // a response recorder for getting written http response
            rr := httptest.NewRecorder()
    
            // create a request body with invalid fields
            reqBody, err := json.Marshal(gin.H{
                "email":    "notanemail",
                "password": "short",
            })
            assert.NoError(t, err)
    
            request, err := http.NewRequest(http.MethodPost, "/signin", bytes.NewBuffer(reqBody))
            assert.NoError(t, err)
    
            request.Header.Set("Content-Type", "application/json")
            router.ServeHTTP(rr, request)
    
            assert.Equal(t, http.StatusBadRequest, rr.Code)
        mockUserService.AssertNotCalled(t, "Signin")
        mockTokenService.AssertNotCalled(t, "NewTokensFromUser")
        })
    

    사용자 서비스 오류.Signin 사례


    이 예에서, 우리는 mockTokenService.NewPairFromUser 에 아날로그 응답을 정의했는데, 이것은 오류를 되돌려줍니다.우리는 이러한 오류의 예시를 만들어 Signin에 저장했다.그런 다음 HTTP-674라고 부르고 status-454라고 부르며 HTTP-674라고 부릅니다.
        t.Run("Error Returned from UserService.Signin", func(t *testing.T) {
            email := "[email protected]"
            password := "pwdoesnotmatch123"
    
            mockUSArgs := mock.Arguments{
                mock.AnythingOfType("*context.emptyCtx"),
                &model.User{Email: email, Password: password},
            }
    
            // so we can check for a known status code
            mockError := apperrors.NewAuthorization("invalid email/password combo")
    
            mockUserService.On("Signin", mockUSArgs...).Return(mockError)
    
            // a response recorder for getting written http response
            rr := httptest.NewRecorder()
    
            // create a request body with valid fields
            reqBody, err := json.Marshal(gin.H{
                "email":    email,
                "password": password,
            })
            assert.NoError(t, err)
    
            request, err := http.NewRequest(http.MethodPost, "/signin", bytes.NewBuffer(reqBody))
            assert.NoError(t, err)
    
            request.Header.Set("Content-Type", "application/json")
            router.ServeHTTP(rr, request)
    
        mockUserService.AssertCalled(t, "Signin", mockUSArgs...)
        mockTokenService.AssertNotCalled(t, "NewTokensFromUser")
            assert.Equal(t, http.StatusUnauthorized, rr.Code)
        })
    

    성공 사례


    이 예에서 우리는 mockError에 대한 응답에 오류가 없다(http.StatusUnauthorized.우리는 또한 mockUserService.Signin 에서 온 유효한 영패 응답을 시뮬레이션했다.우리는 상술한 두 가지 방법을 호출했다고 단언하고 HTTP 상태가 200mockTokenService.NewPairFromUser인 유효한 HTTP 응답을 되돌렸다.
      t.Run("Successful Token Creation", func(t *testing.T) {
            email := "[email protected]"
            password := "pwworksgreat123"
    
            mockUSArgs := mock.Arguments{
                mock.AnythingOfType("*context.emptyCtx"),
                &model.User{Email: email, Password: password},
            }
    
            mockUserService.On("Signin", mockUSArgs...).Return(nil)
    
            mockTSArgs := mock.Arguments{
                mock.AnythingOfType("*context.emptyCtx"),
                &model.User{Email: email, Password: password},
                "",
            }
    
            mockTokenPair := &model.TokenPair{
                IDToken:      "idToken",
                RefreshToken: "refreshToken",
            }
    
            mockTokenService.On("NewPairFromUser", mockTSArgs...).Return(mockTokenPair, nil)
    
            // a response recorder for getting written http response
            rr := httptest.NewRecorder()
    
            // create a request body with valid fields
            reqBody, err := json.Marshal(gin.H{
                "email":    email,
                "password": password,
            })
            assert.NoError(t, err)
    
            request, err := http.NewRequest(http.MethodPost, "/signin", bytes.NewBuffer(reqBody))
            assert.NoError(t, err)
    
            request.Header.Set("Content-Type", "application/json")
            router.ServeHTTP(rr, request)
    
            respBody, err := json.Marshal(gin.H{
                "tokens": mockTokenPair,
            })
            assert.NoError(t, err)
    
            assert.Equal(t, http.StatusOK, rr.Code)
            assert.Equal(t, respBody, rr.Body.Bytes())
    
            mockUserService.AssertCalled(t, "Signin", mockUSArgs...)
            mockTokenService.AssertCalled(t, "NewPairFromUser", mockTSArgs...)
        })
    

    토큰 서비스 오류.NewPairFromUser 사례


    본 예에서 우리는 mockUserService.Signin 방법을 성공적으로 호출하기를 희망하지만 nil에 오류가 발생하기를 희망한다.그리고 우리는 HTTP 500mockTokenService.NewPairFromUser을 응답으로 기대합니다.
      t.Run("Failed Token Creation", func(t *testing.T) {
            email := "[email protected]"
            password := "cannotproducetoken"
    
            mockUSArgs := mock.Arguments{
                mock.AnythingOfType("*context.emptyCtx"),
                &model.User{Email: email, Password: password},
            }
    
            mockUserService.On("Signin", mockUSArgs...).Return(nil)
    
            mockTSArgs := mock.Arguments{
                mock.AnythingOfType("*context.emptyCtx"),
                &model.User{Email: email, Password: password},
                "",
            }
    
            mockError := apperrors.NewInternal()
            mockTokenService.On("NewPairFromUser", mockTSArgs...).Return(nil, mockError)
            // a response recorder for getting written http response
            rr := httptest.NewRecorder()
    
            // create a request body with valid fields
            reqBody, err := json.Marshal(gin.H{
                "email":    email,
                "password": password,
            })
            assert.NoError(t, err)
    
            request, err := http.NewRequest(http.MethodPost, "/signin", bytes.NewBuffer(reqBody))
            assert.NoError(t, err)
    
            request.Header.Set("Content-Type", "application/json")
            router.ServeHTTP(rr, request)
    
            respBody, err := json.Marshal(gin.H{
                "error": mockError,
            })
            assert.NoError(t, err)
    
            assert.Equal(t, mockError.Status(), rr.Code)
            assert.Equal(t, respBody, rr.Body.Bytes())
    
            mockUserService.AssertCalled(t, "Signin", mockUSArgs...)
            mockTokenService.AssertCalled(t, "NewPairFromUser", mockTSArgs...)
        })
    

    결론


    오늘은 이게 다야, 키코스!다음에 우리는 http.StatusOK의 구체적인 실현과 데이터베이스에서 사용자를 얻고 비밀번호를 입력하는 데 필요한 mockUserService.Signin 방법을 작성할 것이다.
    '주, 하스타 루에고!

    좋은 웹페이지 즐겨찾기