// Copyright 2018 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package google.registry.request.auth;

import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import google.registry.security.XsrfTokenManager;
import google.registry.testing.AppEngineRule;
import google.registry.testing.FakeClock;
import javax.servlet.http.HttpServletRequest;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

@RunWith(JUnit4.class)
public final class LegacyAuthenticationMechanismTest {


  @Rule
  public final AppEngineRule appEngine = AppEngineRule.builder()
      .withDatastore()
      .build();

  @Rule public final MockitoRule mocks = MockitoJUnit.rule();

  @Mock private UserService userService;
  @Mock private HttpServletRequest req;

  private final FakeClock clock = new FakeClock();
  private XsrfTokenManager xsrfTokenManager;
  private LegacyAuthenticationMechanism legacyAuthenticationMechanism;
  private String goodToken;

  @Before
  public void setUp() {
    xsrfTokenManager = new XsrfTokenManager(clock, userService);
    legacyAuthenticationMechanism =
        new LegacyAuthenticationMechanism(userService, xsrfTokenManager);
    when(userService.getCurrentUser()).thenReturn(new User("email@example.com", "example.com"));
    when(userService.isUserAdmin()).thenReturn(false);
    goodToken = xsrfTokenManager.generateToken("email@example.com");
  }

  @After
  public void tearDown() {
    // Make sure we didn't use getParameter or getInputStream or any of the other "with side
    // effects" getters unexpectedly. But allow "no side effect" getters.
    //
    // Unfortunately HttpServletRequest doesn't document well which getters "have side effects". It
    // does explicitly state getReader and getInputStream, and that getParameter can also interfere
    // with them, but it doesn't say anything about getParameterNames, getParameterValues,
    // getParameterMap - even though I'm pretty sure they are similar to getParameter in that
    // effect.
    //
    // Feel free to add other "no side effect" functions with atLeast(0) to exempt them from the
    // verifyNoMoreInteractions
    verify(req, atLeast(0)).getMethod();
    verify(req, atLeast(0)).getHeader(any());
    verifyNoMoreInteractions(req);
  }

  @Test
  public void testAuthenticate_notLoggedIn() {
    when(userService.isUserLoggedIn()).thenReturn(false);
    assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
        .isEqualTo(AuthLevel.NONE);
  }

  @Test
  public void testAuthenticate_loggedInSafeMethod_get() {
    when(userService.isUserLoggedIn()).thenReturn(true);
    when(req.getMethod()).thenReturn("GET");
    assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
        .isEqualTo(AuthLevel.USER);
  }

  @Test
  public void testAuthenticate_loggedInSafeMethod_head() {
    when(userService.isUserLoggedIn()).thenReturn(true);
    when(req.getMethod()).thenReturn("HEAD");
    assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
        .isEqualTo(AuthLevel.USER);
  }

  @Test
  public void testAuthenticate_loggedInUnsafeMethod_post_noXsrfToken() {
    when(userService.isUserLoggedIn()).thenReturn(true);
    when(req.getMethod()).thenReturn("POST");
    assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
        .isEqualTo(AuthLevel.NONE);

    // Make sure we looked for the token in all relevant places before giving up
    verify(req).getHeader("X-CSRF-Token");
    verify(req).getParameter("xsrfToken");
  }

  @Test
  public void testAuthenticate_loggedInUnsafeMethod_post_goodTokenInHeader() {
    when(userService.isUserLoggedIn()).thenReturn(true);
    when(req.getMethod()).thenReturn("POST");
    when(req.getHeader("X-CSRF-Token")).thenReturn(goodToken);
    assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
        .isEqualTo(AuthLevel.USER);

    // Make sure we didn't call getParameter (we already verify it in the @After, but we're doing it
    // here explicitly as well for clarity, since this is important in this test)
    verify(req, times(0)).getParameter(any());
  }

  @Test
  public void testAuthenticate_loggedInUnsafeMethod_post_badTokenInHeader() {
    when(userService.isUserLoggedIn()).thenReturn(true);
    when(req.getMethod()).thenReturn("POST");
    when(req.getHeader("X-CSRF-Token")).thenReturn("bad");
    assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
        .isEqualTo(AuthLevel.NONE);

    // Make sure we didn't call getParameter (we already verify it in the @After, but we're doing it
    // here explicitly as well for clarity, since this is important in this test)
    verify(req, times(0)).getParameter(any());
  }

  @Test
  public void testAuthenticate_loggedInUnsafeMethod_post_goodTokenInParam() {
    when(userService.isUserLoggedIn()).thenReturn(true);
    when(req.getMethod()).thenReturn("POST");
    when(req.getParameter("xsrfToken")).thenReturn(goodToken);
    assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
        .isEqualTo(AuthLevel.USER);

    // we allow getParameter to be called in this case (we verify it so it's not caught in the
    // @After's verifyNoMoreInteractions)
    verify(req).getParameter("xsrfToken");
  }

  @Test
  public void testAuthenticate_loggedInUnsafeMethod_post_badTokenInParam() {
    when(userService.isUserLoggedIn()).thenReturn(true);
    when(req.getMethod()).thenReturn("POST");
    when(req.getParameter("xsrfToken")).thenReturn("bad");
    assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
        .isEqualTo(AuthLevel.NONE);

    // we allow getParameter to be called in this case (we verify it so it's not caught in the
    // @After's verifyNoMoreInteractions)
    verify(req).getParameter("xsrfToken");
  }
}
