/*
 *  Copyright 1999-2021 Alibaba Group Holding Ltd.
 *
 *  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 com.alibaba.nacos.core.auth;

import com.alibaba.nacos.api.common.ApiType;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.remote.request.Request;
import com.alibaba.nacos.api.remote.request.RequestMeta;
import com.alibaba.nacos.api.remote.response.HealthCheckResponse;
import com.alibaba.nacos.api.remote.response.Response;
import com.alibaba.nacos.api.remote.response.ResponseCode;
import com.alibaba.nacos.auth.GrpcProtocolAuthService;
import com.alibaba.nacos.auth.annotation.Secured;
import com.alibaba.nacos.auth.config.NacosAuthConfig;
import com.alibaba.nacos.core.context.RequestContextHolder;
import com.alibaba.nacos.core.remote.HealthCheckRequestHandler;
import com.alibaba.nacos.core.remote.RequestHandler;
import com.alibaba.nacos.plugin.auth.api.AuthResult;
import com.alibaba.nacos.plugin.auth.api.IdentityContext;
import com.alibaba.nacos.plugin.auth.api.Permission;
import com.alibaba.nacos.plugin.auth.api.Resource;
import com.alibaba.nacos.plugin.auth.constant.Constants;
import com.alibaba.nacos.sys.env.EnvUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.test.util.ReflectionTestUtils;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

/**
 * {@link RemoteRequestAuthFilter} unit test.
 *
 * @author chenglu
 * @date 2021-07-06 16:14
 */
@ExtendWith(MockitoExtension.class)
class RemoteRequestAuthFilterTest {
    
    private RemoteRequestAuthFilter authFilter;
    
    @Mock
    private NacosAuthConfig authConfig;
    
    @Mock
    private InnerApiAuthEnabled innerApiAuthEnabled;
    
    @Mock
    Request request;
    
    @Mock
    RequestMeta requestMeta;
    
    MockEnvironment environment;
    
    @BeforeEach
    void setUp() {
        environment = new MockEnvironment();
        EnvUtil.setEnvironment(environment);
        environment.setProperty(Constants.Auth.NACOS_CORE_AUTH_ADMIN_ENABLED, "false");
        authFilter = new RemoteRequestAuthFilter(innerApiAuthEnabled);
        GrpcProtocolAuthService protocolAuthService = new GrpcProtocolAuthService(authConfig);
        protocolAuthService.initialize();
        ReflectionTestUtils.setField(authFilter, "protocolAuthService", protocolAuthService);
        ReflectionTestUtils.setField(authFilter, "authConfig", authConfig);
    }
    
    @AfterEach
    void tearDown() {
        RequestContextHolder.removeContext();
        EnvUtil.setEnvironment(null);
    }
    
    @Test
    void testFilterWithoutSecured() throws NacosException {
        Response actual = authFilter.filter(request, requestMeta, HealthCheckRequestHandler.class);
        assertNull(actual);
    }
    
    @Test
    void testFilterDisabledAuth() throws NacosException {
        when(authConfig.isAuthEnabled()).thenReturn(false);
        Response actual = authFilter.filter(request, requestMeta, MockRequestHandler.class);
        assertNull(actual);
        assertEquals(ApiType.OPEN_API.name(), RequestContextHolder.getContext().getAuthContext().getApiType());
    }
    
    @Test
    void testFilterWithoutServerIdentity() throws NacosException {
        when(innerApiAuthEnabled.isEnabled()).thenReturn(true);
        Response actual = authFilter.filter(request, requestMeta, MockInnerRequestHandler.class);
        assertNotNull(actual);
        assertEquals(ResponseCode.FAIL.getCode(), actual.getResultCode());
        assertEquals(403, actual.getErrorCode());
        assertEquals("Invalid server identity key or value, Please make sure set `nacos.core.auth.server.identity.key`"
                        + " and `nacos.core.auth.server.identity.value`, or open `nacos.core.auth.enable.userAgentAuthWhite`",
                actual.getMessage());
    }
    
    @Test
    void testFilterDisabledAuthWithInnerApi() throws NacosException {
        when(innerApiAuthEnabled.isEnabled()).thenReturn(true);
        when(authConfig.getServerIdentityKey()).thenReturn("1");
        when(authConfig.getServerIdentityValue()).thenReturn("2");
        Response actual = authFilter.filter(request, requestMeta, MockInnerRequestHandler.class);
        assertNull(actual);
    }
    
    @Test
    void testFilterWithoutServerIdentityForInnerApiAuthDisabled() throws NacosException {
        when(innerApiAuthEnabled.isEnabled()).thenReturn(false);
        Response actual = authFilter.filter(request, requestMeta, MockInnerRequestHandler.class);
        assertNull(actual);
    }
    
    @Test
    void testFilterWithServerIdentity() throws NacosException {
        when(innerApiAuthEnabled.isEnabled()).thenReturn(true);
        when(authConfig.getServerIdentityKey()).thenReturn("1");
        when(authConfig.getServerIdentityValue()).thenReturn("2");
        when(request.getHeader("1")).thenReturn("2");
        Response actual = authFilter.filter(request, requestMeta, MockInnerRequestHandler.class);
        assertNull(actual);
    }
    
    @Test
    void testFilterWithNoNeedAuthSecured() throws NacosException {
        when(authConfig.isAuthEnabled()).thenReturn(true);
        GrpcProtocolAuthService protocolAuthService = injectMockPlugins();
        when(protocolAuthService.enableAuth(any(Secured.class))).thenReturn(false);
        Response actual = authFilter.filter(request, requestMeta, MockRequestHandler.class);
        assertNull(actual);
    }
    
    @Test
    void testFilterWithNeedAuthSecuredSuccess() throws NacosException {
        when(authConfig.isAuthEnabled()).thenReturn(true);
        GrpcProtocolAuthService protocolAuthService = injectMockPlugins();
        when(protocolAuthService.enableAuth(any(Secured.class))).thenReturn(true);
        doReturn(new IdentityContext()).when(protocolAuthService).parseIdentity(eq(request));
        doReturn(Resource.EMPTY_RESOURCE).when(protocolAuthService).parseResource(eq(request), any(Secured.class));
        when(protocolAuthService.validateIdentity(any(IdentityContext.class), any(Resource.class))).thenReturn(
                AuthResult.successResult());
        when(protocolAuthService.validateAuthority(any(IdentityContext.class), any(Permission.class))).thenReturn(
                AuthResult.successResult());
        Response actual = authFilter.filter(request, requestMeta, MockRequestHandler.class);
        assertNull(actual);
    }
    
    @Test
    @Secured
    void testFilterWithNeedAuthSecuredIdentityFailure() throws NacosException {
        when(authConfig.isAuthEnabled()).thenReturn(true);
        GrpcProtocolAuthService protocolAuthService = injectMockPlugins();
        when(protocolAuthService.enableAuth(any(Secured.class))).thenReturn(true);
        doReturn(new IdentityContext()).when(protocolAuthService).parseIdentity(eq(request));
        doReturn(Resource.EMPTY_RESOURCE).when(protocolAuthService).parseResource(eq(request), any(Secured.class));
        when(protocolAuthService.validateIdentity(any(IdentityContext.class), any(Resource.class))).thenReturn(
                AuthResult.failureResult(403, "test"));
        Response actual = authFilter.filter(request, requestMeta, MockRequestHandler.class);
        assertNotNull(actual);
        assertEquals(ResponseCode.FAIL.getCode(), actual.getResultCode());
        assertEquals(403, actual.getErrorCode());
    }
    
    @Test
    @Secured
    void testDoFilterWithNeedAuthSecuredAuthorityFailure() throws NacosException {
        when(authConfig.isAuthEnabled()).thenReturn(true);
        GrpcProtocolAuthService protocolAuthService = injectMockPlugins();
        when(protocolAuthService.enableAuth(any(Secured.class))).thenReturn(true);
        doReturn(new IdentityContext()).when(protocolAuthService).parseIdentity(eq(request));
        doReturn(Resource.EMPTY_RESOURCE).when(protocolAuthService).parseResource(eq(request), any(Secured.class));
        when(protocolAuthService.validateIdentity(any(IdentityContext.class), any(Resource.class))).thenReturn(
                AuthResult.successResult());
        when(protocolAuthService.validateAuthority(any(IdentityContext.class), any(Permission.class))).thenReturn(
                AuthResult.failureResult(403, "test"));
        Response actual = authFilter.filter(request, requestMeta, MockRequestHandler.class);
        assertNotNull(actual);
        assertEquals(ResponseCode.FAIL.getCode(), actual.getResultCode());
        assertEquals(403, actual.getErrorCode());
    }
    
    private GrpcProtocolAuthService injectMockPlugins() {
        GrpcProtocolAuthService protocolAuthService = new GrpcProtocolAuthService(authConfig);
        protocolAuthService.initialize();
        GrpcProtocolAuthService spyProtocolAuthService = spy(protocolAuthService);
        ReflectionTestUtils.setField(authFilter, "protocolAuthService", spyProtocolAuthService);
        return spyProtocolAuthService;
    }
    
    static class MockRequestHandler extends RequestHandler<Request, HealthCheckResponse> {
        
        @Secured(resource = "xxx")
        @Override
        public HealthCheckResponse handle(Request request, RequestMeta meta) throws NacosException {
            return new HealthCheckResponse();
        }
    }
    
    static class MockInnerRequestHandler extends RequestHandler<Request, HealthCheckResponse> {
        
        @Secured(resource = "xxx", apiType = ApiType.INNER_API)
        @Override
        public HealthCheckResponse handle(Request request, RequestMeta meta) throws NacosException {
            return new HealthCheckResponse();
        }
    }
}
