/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.hadoop.hdfs.web;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.DelegationTokenRenewer;
import org.apache.hadoop.fs.DelegationTokenRenewer.RenewAction;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.security.token.TokenIdentifier;
import org.apache.hadoop.util.Progressable;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.internal.util.reflection.Whitebox;

public class TestTokenAspect {

  private static class DummyFs extends FileSystem implements
      DelegationTokenRenewer.Renewable, TokenAspect.TokenManagementDelegator {

    private static final Text TOKEN_KIND = new Text("DummyFS Token");
    private boolean emulateSecurityEnabled;
    private TokenAspect<DummyFs> tokenAspect;
    private final UserGroupInformation ugi = UserGroupInformation
        .createUserForTesting("foo", new String[] { "bar" });
    private URI uri;

    @Override
    public FSDataOutputStream append(Path f, int bufferSize,
        Progressable progress) throws IOException {
      return null;
    }

    @Override
    public void cancelDelegationToken(Token<?> token) throws IOException {
    }

    @Override
    public FSDataOutputStream create(Path f, FsPermission permission,
        boolean overwrite, int bufferSize, short replication, long blockSize,
        Progressable progress) throws IOException {
      return null;
    }

    @Override
    public boolean delete(Path f, boolean recursive) throws IOException {
      return false;
    }

    @Override
    public URI getCanonicalUri() {
      return super.getCanonicalUri();
    }

    @Override
    public FileStatus getFileStatus(Path f) throws IOException {
      return null;
    }

    @Override
    public Token<?> getRenewToken() {
      return null;
    }

    @Override
    public URI getUri() {
      return uri;
    }

    @Override
    public Path getWorkingDirectory() {
      return null;
    }

    @Override
    public void initialize(URI name, Configuration conf) throws IOException {
      super.initialize(name, conf);
      setConf(conf);
      this.uri = URI.create(name.getScheme() + "://" + name.getAuthority());
      tokenAspect = new TokenAspect<DummyFs>(this,
          SecurityUtil.buildTokenService(uri), TOKEN_KIND);
      if (emulateSecurityEnabled || UserGroupInformation.isSecurityEnabled()) {
        tokenAspect.initDelegationToken(ugi);
      }
    }

    @Override
    public FileStatus[] listStatus(Path f) throws IOException {
      return new FileStatus[0];
    }

    @Override
    public boolean mkdirs(Path f, FsPermission permission) throws IOException {
      return false;
    }

    @Override
    public FSDataInputStream open(Path f, int bufferSize) throws IOException {
      return null;
    }

    @Override
    public boolean rename(Path src, Path dst) throws IOException {
      return false;
    }

    @Override
    public long renewDelegationToken(Token<?> token) throws IOException {
      return 0;
    }

    @Override
    public <T extends TokenIdentifier> void setDelegationToken(Token<T> token) {
    }

    @Override
    public void setWorkingDirectory(Path new_dir) {
    }
  }

  private static RenewAction<?> getActionFromTokenAspect(
      TokenAspect<DummyFs> tokenAspect) {
    return (RenewAction<?>) Whitebox.getInternalState(tokenAspect, "action");
  }

  @Test
  public void testCachedInitialization() throws IOException, URISyntaxException {
    Configuration conf = new Configuration();
    DummyFs fs = spy(new DummyFs());
    Token<TokenIdentifier> token = new Token<TokenIdentifier>(new byte[0],
        new byte[0], DummyFs.TOKEN_KIND, new Text("127.0.0.1:1234"));

    doReturn(token).when(fs).getDelegationToken(anyString());
    doReturn(token).when(fs).getRenewToken();

    fs.emulateSecurityEnabled = true;
    fs.initialize(new URI("dummyfs://127.0.0.1:1234"), conf);

    fs.tokenAspect.ensureTokenInitialized();
    verify(fs, times(1)).getDelegationToken(null);
    verify(fs, times(1)).setDelegationToken(token);

    // For the second iteration, the token should be cached.
    fs.tokenAspect.ensureTokenInitialized();
    verify(fs, times(1)).getDelegationToken(null);
    verify(fs, times(1)).setDelegationToken(token);
  }

  @Test
  public void testGetRemoteToken() throws IOException, URISyntaxException {
    Configuration conf = new Configuration();
    DummyFs fs = spy(new DummyFs());
    Token<TokenIdentifier> token = new Token<TokenIdentifier>(new byte[0],
        new byte[0], DummyFs.TOKEN_KIND, new Text("127.0.0.1:1234"));

    doReturn(token).when(fs).getDelegationToken(anyString());
    doReturn(token).when(fs).getRenewToken();

    fs.initialize(new URI("dummyfs://127.0.0.1:1234"), conf);

    fs.tokenAspect.ensureTokenInitialized();

    // Select a token, store and renew it
    verify(fs).setDelegationToken(token);
    assertNotNull(Whitebox.getInternalState(fs.tokenAspect, "dtRenewer"));
    assertNotNull(Whitebox.getInternalState(fs.tokenAspect, "action"));
  }

  @Test
  public void testGetRemoteTokenFailure() throws IOException,
      URISyntaxException {
    Configuration conf = new Configuration();
    DummyFs fs = spy(new DummyFs());
    IOException e = new IOException();
    doThrow(e).when(fs).getDelegationToken(anyString());

    fs.emulateSecurityEnabled = true;
    fs.initialize(new URI("dummyfs://127.0.0.1:1234"), conf);
    try {
      fs.tokenAspect.ensureTokenInitialized();
    } catch (IOException exc) {
      assertEquals(e, exc);
    }
  }

  @Test
  public void testInitWithNoTokens() throws IOException, URISyntaxException {
    Configuration conf = new Configuration();
    DummyFs fs = spy(new DummyFs());
    doReturn(null).when(fs).getDelegationToken(anyString());
    fs.initialize(new URI("dummyfs://127.0.0.1:1234"), conf);

    fs.tokenAspect.ensureTokenInitialized();

    // No token will be selected.
    verify(fs, never()).setDelegationToken(
        Mockito.<Token<? extends TokenIdentifier>> any());
  }

  @Test
  public void testInitWithUGIToken() throws IOException, URISyntaxException {
    Configuration conf = new Configuration();
    DummyFs fs = spy(new DummyFs());
    doReturn(null).when(fs).getDelegationToken(anyString());

    Token<TokenIdentifier> token = new Token<TokenIdentifier>(new byte[0],
        new byte[0], DummyFs.TOKEN_KIND, new Text("127.0.0.1:1234"));
    fs.ugi.addToken(token);
    fs.ugi.addToken(new Token<TokenIdentifier>(new byte[0], new byte[0],
        new Text("Other token"), new Text("127.0.0.1:8021")));
    assertEquals("wrong tokens in user", 2, fs.ugi.getTokens().size());

    fs.emulateSecurityEnabled = true;
    fs.initialize(new URI("dummyfs://127.0.0.1:1234"), conf);
    fs.tokenAspect.ensureTokenInitialized();

    // Select a token from ugi (not from the remote host), store it but don't
    // renew it
    verify(fs).setDelegationToken(token);
    verify(fs, never()).getDelegationToken(anyString());
    assertNull(Whitebox.getInternalState(fs.tokenAspect, "dtRenewer"));
    assertNull(Whitebox.getInternalState(fs.tokenAspect, "action"));
  }

  @Test
  public void testRenewal() throws Exception {
    Configuration conf = new Configuration();
    Token<?> token1 = mock(Token.class);
    Token<?> token2 = mock(Token.class);
    final long renewCycle = 100;
    DelegationTokenRenewer.renewCycle = renewCycle;

    UserGroupInformation ugi = UserGroupInformation.createUserForTesting("foo",
        new String[] { "bar" });
    DummyFs fs = spy(new DummyFs());

    doReturn(token1).doReturn(token2).when(fs).getDelegationToken(null);
    doReturn(token1).when(fs).getRenewToken();
    // cause token renewer to abandon the token
    doThrow(new IOException("renew failed")).when(token1).renew(conf);
    doThrow(new IOException("get failed")).when(fs).addDelegationTokens(null,
        null);

    final URI uri = new URI("dummyfs://127.0.0.1:1234");
    TokenAspect<DummyFs> tokenAspect = new TokenAspect<DummyFs>(fs,
        SecurityUtil.buildTokenService(uri), DummyFs.TOKEN_KIND);
    fs.initialize(uri, conf);
    tokenAspect.initDelegationToken(ugi);

    // trigger token acquisition
    tokenAspect.ensureTokenInitialized();
    DelegationTokenRenewer.RenewAction<?> action = getActionFromTokenAspect(tokenAspect);
    verify(fs).setDelegationToken(token1);
    assertTrue(action.isValid());

    // upon renewal, token will go bad based on above stubbing
    Thread.sleep(renewCycle * 2);
    assertSame(action, getActionFromTokenAspect(tokenAspect));
    assertFalse(action.isValid());

    // now that token is invalid, should get a new one
    tokenAspect.ensureTokenInitialized();
    verify(fs, times(2)).getDelegationToken(anyString());
    verify(fs).setDelegationToken(token2);
    assertNotSame(action, getActionFromTokenAspect(tokenAspect));

    action = getActionFromTokenAspect(tokenAspect);
    assertTrue(action.isValid());
  }
}
