Files
hanko/frontend/frontend-sdk/tests/lib/client/HttpClient.spec.ts
Matthew H. Irby e7a5c2df27 Feat: Add logout method to sdk (#566)
* Add endpoint to invalidate HTTP-Only cookie from the backend

* Add methods to the UserClient SDK for logout

* Remove session token fetch and add unit test for logout

* Update public router to use JWT middleware

* Add logout button to frontend. Route back to login page once logout is successful.

* Add a logout failur event

* Update logout logic in SDK

* Remove unneeded endpoint from main.go

* Update logoutlink reference

* Fix request path; undo change in package order

* Update common.css to incldue hanko-logout

* feat(fronend-sdk): remove cookie during cross-domain operations

* fix(frontend-sdk): No unauthorized error during logout, when the user is already logged out

* feat(backend): Create an audit log entry when the user logs off

* chore(frontend-sdk): re-generate jsdoc

* fix: Adjust logout response codes and the corresponding frontend sdk error handling

* chore(frontend-sdk): re-generate jsdoc

* feat: add logout endpoint specification to the docs

* Fix broken unit test

* Remove logout button from elements

* Add event listener on frontend to call the logout method from SDK

* Rollback changes to SecuredContent on e2e tests

* Update logout test on user

* Update quickstart/public/assets/css/common.css

Co-authored-by: bjoern-m <56024829+bjoern-m@users.noreply.github.com>

---------

Co-authored-by: Björn Müller <bjoern.mueller@hanko.io>
Co-authored-by: bjoern-m <56024829+bjoern-m@users.noreply.github.com>
2023-03-03 10:48:33 +01:00

236 lines
6.8 KiB
TypeScript

import {
Headers,
Response,
HttpClient,
} from "../../../src/lib/client/HttpClient";
import { RequestTimeoutError, TechnicalError } from "../../../src";
import Cookies from "js-cookie";
const jwt = "test-token";
let httpClient: HttpClient;
let xhr: XMLHttpRequest;
beforeEach(() => {
Object.defineProperty(global, "XMLHttpRequest", {
value: jest.fn().mockImplementation(() => ({
response: JSON.stringify({ foo: "bar" }),
open: jest.fn(),
setRequestHeader: jest.fn(),
getResponseHeader: jest.fn(),
getAllResponseHeaders: jest.fn().mockReturnValue(`X-Auth-Token: ${jwt}`),
send: jest.fn(),
})),
configurable: true,
writable: true,
});
httpClient = new HttpClient("http://test.api");
xhr = new XMLHttpRequest();
});
describe("httpClient._fetch()", () => {
it("should perform http requests", async () => {
jest.spyOn(xhr, "send").mockImplementation(function () {
// eslint-disable-next-line no-invalid-this
this.onload();
});
const response = await httpClient._fetch("/test", { method: "GET" }, xhr);
expect(xhr.setRequestHeader).toHaveBeenCalledWith(
"Accept",
"application/json"
);
expect(xhr.setRequestHeader).toHaveBeenCalledWith(
"Content-Type",
"application/json"
);
expect(xhr.setRequestHeader).toHaveBeenCalledTimes(2);
expect(xhr.getAllResponseHeaders).toHaveBeenCalledTimes(1);
expect(xhr.open).toHaveBeenNthCalledWith(
1,
"GET",
"http://test.api/test",
true
);
expect(response.json()).toEqual({ foo: "bar" });
});
it("should set authorization request headers when cookie is available", async () => {
jest.spyOn(xhr, "send").mockImplementation(function () {
// eslint-disable-next-line no-invalid-this
this.onload();
});
jest.spyOn(httpClient, "_getAuthCookie").mockReturnValue(jwt);
await httpClient._fetch("/test", { method: "GET" }, xhr);
expect(xhr.setRequestHeader).toHaveBeenCalledWith(
"Authorization",
`Bearer ${jwt}`
);
expect(xhr.setRequestHeader).toHaveBeenCalledTimes(3);
});
it("should set a cookie if x-auth-token response header is available", async () => {
const jwt = "test-token";
const xhr = new XMLHttpRequest();
const client = new HttpClient("http://test.api");
jest.spyOn(xhr, "send").mockImplementation(function () {
// eslint-disable-next-line no-invalid-this
this.onload();
});
jest.spyOn(xhr, "getResponseHeader").mockReturnValue(jwt);
jest.spyOn(client, "_setAuthCookie");
await client._fetch("/test", { method: "GET" }, xhr);
expect(xhr.getResponseHeader).toHaveBeenCalledWith("X-Auth-Token");
expect(client._setAuthCookie).toHaveBeenCalledWith(jwt);
});
it("should handle onerror", async () => {
jest.spyOn(xhr, "send").mockImplementation(function () {
// eslint-disable-next-line no-invalid-this
this.onerror();
});
const response = httpClient._fetch("/test", { method: "GET" }, xhr);
await expect(response).rejects.toThrow(TechnicalError);
});
it("should handle ontimeout", async () => {
jest.spyOn(xhr, "send").mockImplementation(function () {
// eslint-disable-next-line no-invalid-this
this.ontimeout();
});
const response = httpClient._fetch("/test", { method: "GET" }, xhr);
await expect(response).rejects.toThrow(RequestTimeoutError);
});
});
describe("httpClient._setAuthCookie()", () => {
it("should set a new cookie", async () => {
httpClient = new HttpClient("http://test.api");
jest.spyOn(Cookies, "set");
httpClient._setAuthCookie("test-token");
expect(Cookies.set).toHaveBeenCalledWith("hanko", "test-token", {
secure: false,
});
});
it("should set a new secure cookie", async () => {
httpClient = new HttpClient("https://test.api");
jest.spyOn(Cookies, "set");
httpClient._setAuthCookie("test-token");
expect(Cookies.set).toHaveBeenCalledWith("hanko", "test-token", {
secure: true,
});
});
});
describe("httpClient._getAuthCookie()", () => {
it("should return the contents of the authorization cookie", async () => {
httpClient = new HttpClient("https://test.api");
Cookies.get = jest.fn().mockReturnValue("test-token");
const token = httpClient._getAuthCookie();
expect(Cookies.get).toHaveBeenCalledWith("hanko");
expect(token).toBe("test-token");
});
});
describe("httpClient._removeAuthCookie()", () => {
it("should return the contents of the authorization cookie", async () => {
httpClient = new HttpClient("https://test.api");
jest.spyOn(Cookies, "remove");
httpClient.removeAuthCookie();
expect(Cookies.remove).toHaveBeenCalledWith("hanko");
});
});
describe("httpClient.get()", () => {
it("should call get with correct args", async () => {
httpClient._fetch = jest.fn();
await httpClient.get("/test");
expect(httpClient._fetch).toHaveBeenCalledWith("/test", { method: "GET" });
});
});
describe("httpClient.post()", () => {
it("should call post with correct args", async () => {
httpClient._fetch = jest.fn();
await httpClient.post("/test");
expect(httpClient._fetch).toHaveBeenCalledWith("/test", { method: "POST" });
});
});
describe("httpClient.put()", () => {
it("should call put with correct args", async () => {
httpClient._fetch = jest.fn();
await httpClient.put("/test");
expect(httpClient._fetch).toHaveBeenCalledWith("/test", { method: "PUT" });
});
});
describe("httpClient.patch()", () => {
it("should call patch with correct args", async () => {
httpClient._fetch = jest.fn();
await httpClient.patch("/test");
expect(httpClient._fetch).toHaveBeenCalledWith("/test", {
method: "PATCH",
});
});
});
describe("httpClient.delete()", () => {
it("should call delete with correct args", async () => {
httpClient._fetch = jest.fn();
await httpClient.delete("/test");
expect(httpClient._fetch).toHaveBeenCalledWith("/test", {
method: "DELETE",
});
});
});
describe("headers.get()", () => {
it("should return headers", async () => {
const header = new Headers(xhr);
jest.spyOn(xhr, "getResponseHeader").mockReturnValue("bar");
expect(header.get("foo")).toEqual("bar");
});
});
describe("response.parseRetryAfterHeader()", () => {
it.each`
headerValue | expected
${""} | ${0}
${"0"} | ${0}
${"3"} | ${3}
${"-3"} | ${-3}
${"invalid"} | ${0}
`("should parse retry-after header", async ({ headerValue, expected }) => {
const response = new Response(xhr);
jest.spyOn(xhr, "getResponseHeader").mockReturnValue(headerValue);
const result = response.parseRetryAfterHeader();
expect(xhr.getResponseHeader).toHaveBeenCalledWith("Retry-After");
expect(result).toBe(expected);
});
});