mirror of
https://github.com/teamhanko/hanko.git
synced 2025-10-27 14:17:56 +08:00
* 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>
236 lines
6.8 KiB
TypeScript
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);
|
|
});
|
|
});
|