
import Assert = require("assert");
import VerifyGet = require("../../../src/lib/routes/verify/get");
import { AuthenticationSessionHandler } from "../../../src/lib/AuthenticationSessionHandler";
import { AuthenticationSession } from "../../../types/AuthenticationSession";
import { AuthenticationMethodsConfiguration } from "../../../src/lib/configuration/Configuration";
import Sinon = require("sinon");
import winston = require("winston");
import BluebirdPromise = require("bluebird");
import express = require("express");
import ExpressMock = require("../../mocks/express");
import { ServerVariables } from "../../../src/lib/ServerVariables";
import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../mocks/ServerVariablesMockBuilder";

describe("test /api/verify endpoint", function () {
  let req: ExpressMock.RequestMock;
  let res: ExpressMock.ResponseMock;
  let mocks: ServerVariablesMock;
  let vars: ServerVariables;
  let authSession: AuthenticationSession;

  beforeEach(function () {
    req = ExpressMock.RequestMock();
    res = ExpressMock.ResponseMock();
    req.originalUrl = "/api/xxxx";
    req.query = {
      redirect: "undefined"
    };
    AuthenticationSessionHandler.reset(req as any);
    req.headers["x-original-url"] = "https://secret.example.com/";
    const s = ServerVariablesMockBuilder.build(false);
    mocks = s.mocks;
    vars = s.variables;
    authSession = AuthenticationSessionHandler.get(req as any, vars.logger);
  });

  describe("with session cookie", function () {
    beforeEach(function () {
      vars.config.authentication_methods.default_method = "two_factor";
    });

    it("should be already authenticated", function () {
      mocks.accessController.isAccessAllowedMock.returns(true);
      authSession.first_factor = true;
      authSession.second_factor = true;
      authSession.userid = "myuser";
      authSession.groups = ["mygroup", "othergroup"];
      return VerifyGet.default(vars)(req as express.Request, res as any)
        .then(function () {
          Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "myuser");
          Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup");
          Assert.equal(204, res.status.getCall(0).args[0]);
        });
    });

    function test_session(_authSession: AuthenticationSession, status_code: number) {
      return VerifyGet.default(vars)(req as express.Request, res as any)
        .then(function () {
          Assert.equal(status_code, res.status.getCall(0).args[0]);
        });
    }

    function test_non_authenticated_401(authSession: AuthenticationSession) {
      return test_session(authSession, 401);
    }

    function test_unauthorized_403(authSession: AuthenticationSession) {
      return test_session(authSession, 403);
    }

    function test_authorized(authSession: AuthenticationSession) {
      return test_session(authSession, 204);
    }

    describe("given user tries to access a 2-factor endpoint", function () {
      before(function () {
        mocks.accessController.isAccessAllowedMock.returns(true);
      });

      describe("given different cases of session", function () {
        it("should not be authenticated when second factor is missing", function () {
          return test_non_authenticated_401({
            userid: "user",
            first_factor: true,
            second_factor: false,
            email: undefined,
            groups: [],
            last_activity_datetime: new Date().getTime()
          });
        });

        it("should not be authenticated when first factor is missing", function () {
          return test_non_authenticated_401({
            userid: "user",
            first_factor: false,
            second_factor: true,
            email: undefined,
            groups: [],
            last_activity_datetime: new Date().getTime()
          });
        });

        it("should not be authenticated when userid is missing", function () {
          return test_non_authenticated_401({
            userid: undefined,
            first_factor: true,
            second_factor: false,
            email: undefined,
            groups: [],
            last_activity_datetime: new Date().getTime()
          });
        });

        it("should not be authenticated when first and second factor are missing", function () {
          return test_non_authenticated_401({
            userid: "user",
            first_factor: false,
            second_factor: false,
            email: undefined,
            groups: [],
            last_activity_datetime: new Date().getTime()
          });
        });

        it("should not be authenticated when session has not be initiated", function () {
          return test_non_authenticated_401(undefined);
        });

        it("should not be authenticated when domain is not allowed for user", function () {
          authSession.first_factor = true;
          authSession.second_factor = true;
          authSession.userid = "myuser";
          req.headers["x-original-url"] = "https://test.example.com/";
          mocks.accessController.isAccessAllowedMock.returns(false);

          return test_unauthorized_403({
            first_factor: true,
            second_factor: true,
            userid: "user",
            groups: ["group1", "group2"],
            email: undefined,
            last_activity_datetime: new Date().getTime()
          });
        });
      });
    });

    describe("given user tries to access a single factor endpoint", function () {
      beforeEach(function () {
        req.headers["x-original-url"] = "https://redirect.url/";
        mocks.config.authentication_methods.per_subdomain_methods = {
          "redirect.url": "single_factor"
        };
      });

      it("should be authenticated when first factor is validated and second factor is not", function () {
        mocks.accessController.isAccessAllowedMock.returns(true);
        authSession.first_factor = true;
        authSession.userid = "user1";
        return VerifyGet.default(vars)(req as express.Request, res as any)
          .then(function () {
            Assert(res.status.calledWith(204));
            Assert(res.send.calledOnce);
          });
      });

      it("should be rejected with 401 when first factor is not validated", function () {
        mocks.accessController.isAccessAllowedMock.returns(true);
        authSession.first_factor = false;
        return VerifyGet.default(vars)(req as express.Request, res as any)
          .then(function () {
            Assert(res.status.calledWith(401));
          });
      });
    });

    describe("inactivity period", function () {
      it("should update last inactivity period on requests on /api/verify", function () {
        mocks.config.session.inactivity = 200000;
        mocks.accessController.isAccessAllowedMock.returns(true);
        const currentTime = new Date().getTime() - 1000;
        AuthenticationSessionHandler.reset(req as any);
        authSession.first_factor = true;
        authSession.second_factor = true;
        authSession.userid = "myuser";
        authSession.groups = ["mygroup", "othergroup"];
        authSession.last_activity_datetime = currentTime;
        return VerifyGet.default(vars)(req as express.Request, res as any)
          .then(function () {
            return AuthenticationSessionHandler.get(req as any, vars.logger);
          })
          .then(function (authSession) {
            Assert(authSession.last_activity_datetime > currentTime);
          });
      });

      it("should reset session when max inactivity period has been reached", function () {
        mocks.config.session.inactivity = 1;
        mocks.accessController.isAccessAllowedMock.returns(true);
        const currentTime = new Date().getTime() - 1000;
        AuthenticationSessionHandler.reset(req as any);
        authSession.first_factor = true;
        authSession.second_factor = true;
        authSession.userid = "myuser";
        authSession.groups = ["mygroup", "othergroup"];
        authSession.last_activity_datetime = currentTime;
        return VerifyGet.default(vars)(req as express.Request, res as any)
          .then(function () {
            return AuthenticationSessionHandler.get(req as any, vars.logger);
          })
          .then(function (authSession) {
            Assert.equal(authSession.first_factor, false);
            Assert.equal(authSession.second_factor, false);
            Assert.equal(authSession.userid, undefined);
          });
      });
    });
  });

  describe("response type 401 | 302", function() {
    it("should return error code 401", function() {
      mocks.accessController.isAccessAllowedMock.returns(true);
      mocks.config.authentication_methods.default_method = "single_factor";
      mocks.ldapAuthenticator.authenticateStub.rejects(new Error(
        "Invalid credentials"));
      req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";

      return VerifyGet.default(vars)(req as express.Request, res as any)
        .then(function () {
          Assert(res.status.calledWithExactly(401));
        });
    });

    it("should redirect to provided redirection url", function() {
      const REDIRECT_URL = "http://redirection_url.com";
      mocks.accessController.isAccessAllowedMock.returns(true);
      mocks.config.authentication_methods.default_method = "single_factor";
      mocks.ldapAuthenticator.authenticateStub.rejects(new Error(
        "Invalid credentials"));
      req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
      req.query["rd"] = REDIRECT_URL;

      return VerifyGet.default(vars)(req as express.Request, res as any)
        .then(function () {
          Assert(res.redirect.calledWithExactly(REDIRECT_URL));
        });
    });
  });

  describe("with basic auth", function () {
    it("should authenticate correctly", function () {
      mocks.accessController.isAccessAllowedMock.returns(true);
      mocks.config.authentication_methods.default_method = "single_factor";
      mocks.ldapAuthenticator.authenticateStub.returns({
        groups: ["mygroup", "othergroup"],
      });
      req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";

      return VerifyGet.default(vars)(req as express.Request, res as any)
        .then(function () {
          Sinon.assert.calledWithExactly(res.setHeader, "Remote-User", "john");
          Sinon.assert.calledWithExactly(res.setHeader, "Remote-Groups", "mygroup,othergroup");
          Assert.equal(204, res.status.getCall(0).args[0]);
        });
    });

    it("should fail when endpoint is protected by two factors", function () {
      mocks.accessController.isAccessAllowedMock.returns(true);
      mocks.config.authentication_methods.default_method = "single_factor";
      mocks.config.authentication_methods.per_subdomain_methods = {
        "secret.example.com": "two_factor"
      };
      mocks.ldapAuthenticator.authenticateStub.resolves({
        groups: ["mygroup", "othergroup"],
      });
      req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";

      return VerifyGet.default(vars)(req as express.Request, res as any)
        .then(function () {
          Assert(res.status.calledWithExactly(401));
        });
    });

    it("should fail when base64 token is not valid", function () {
      mocks.accessController.isAccessAllowedMock.returns(true);
      mocks.config.authentication_methods.default_method = "single_factor";
      mocks.ldapAuthenticator.authenticateStub.resolves({
        groups: ["mygroup", "othergroup"],
      });
      req.headers["proxy-authorization"] = "Basic i_m*not_a_base64*token";

      return VerifyGet.default(vars)(req as express.Request, res as any)
        .then(function () {
          Assert(res.status.calledWithExactly(401));
        });
    });

    it("should fail when base64 token has not format user:psswd", function () {
      mocks.accessController.isAccessAllowedMock.returns(true);
      mocks.config.authentication_methods.default_method = "single_factor";
      mocks.ldapAuthenticator.authenticateStub.resolves({
        groups: ["mygroup", "othergroup"],
      });
      req.headers["proxy-authorization"] = "Basic am9objpwYXNzOmJhZA==";

      return VerifyGet.default(vars)(req as express.Request, res as any)
        .then(function () {
          Assert(res.status.calledWithExactly(401));
        });
    });

    it("should fail when bad user password is provided", function () {
      mocks.accessController.isAccessAllowedMock.returns(true);
      mocks.config.authentication_methods.default_method = "single_factor";
      mocks.ldapAuthenticator.authenticateStub.rejects(new Error(
        "Invalid credentials"));
      req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";

      return VerifyGet.default(vars)(req as express.Request, res as any)
        .then(function () {
          Assert(res.status.calledWithExactly(401));
        });
    });

    it("should fail when resource is restricted", function () {
      mocks.accessController.isAccessAllowedMock.returns(false);
      mocks.config.authentication_methods.default_method = "single_factor";
      mocks.ldapAuthenticator.authenticateStub.resolves({
        groups: ["mygroup", "othergroup"],
      });
      req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";

      return VerifyGet.default(vars)(req as express.Request, res as any)
        .then(function () {
          Assert(res.status.calledWithExactly(401));
        });
    });
  });
});

