From 6ed1588b31b9e06dbd1a6143bd19f20d4ddaf419 Mon Sep 17 00:00:00 2001 From: David Farr Date: Wed, 22 May 2024 11:08:37 -0700 Subject: [PATCH] Add support for basic auth (#118) * Add support for basic auth --- lib/core/errors.ts | 13 +- lib/core/options.ts | 59 +++- lib/core/stores/local.ts | 25 +- lib/core/stores/remote.ts | 499 +++++++++++++------------------- lib/resonate.ts | 16 +- test/auth.test.ts | 59 ++++ test/promiseTransitions.test.ts | 13 +- test/promises.test.ts | 13 +- test/schedules.test.ts | 12 +- 9 files changed, 374 insertions(+), 335 deletions(-) create mode 100644 test/auth.test.ts diff --git a/lib/core/errors.ts b/lib/core/errors.ts index ada1505..3036983 100644 --- a/lib/core/errors.ts +++ b/lib/core/errors.ts @@ -13,12 +13,13 @@ export enum ErrorCodes { // store STORE = 40, - STORE_PAYLOAD = 41, - STORE_FORBIDDEN = 42, - STORE_NOT_FOUND = 43, - STORE_ALREADY_EXISTS = 44, - STORE_INVALID_STATE = 45, - STORE_ENCODER = 46, + STORE_UNAUTHORIZED = 41, + STORE_PAYLOAD = 42, + STORE_FORBIDDEN = 43, + STORE_NOT_FOUND = 44, + STORE_ALREADY_EXISTS = 45, + STORE_INVALID_STATE = 46, + STORE_ENCODER = 47, } export class ResonateError extends Error { diff --git a/lib/core/options.ts b/lib/core/options.ts index 1a4230f..f236f4f 100644 --- a/lib/core/options.ts +++ b/lib/core/options.ts @@ -7,6 +7,11 @@ import { IStore } from "./store"; * Resonate configuration options. */ export type ResonateOptions = { + /** + * Store authentication options. + */ + auth: AuthOptions; + /** * An encoder instance used for encoding and decoding values * returned (or thrown) by registered functions. If not provided, @@ -14,6 +19,11 @@ export type ResonateOptions = { */ encoder: IEncoder; + /** + * The frequency in ms to heartbeat locks. + */ + heartbeat: number; + /** * A process id that can be used to uniquely identify this Resonate * instance. If not provided a default value will be generated. @@ -43,8 +53,8 @@ export type ResonateOptions = { tags: Record; /** - * A store instance, if provided this will take precedence over a - * remote store. + * A store instance, if provided will take predence over the + * default store. */ store: IStore; @@ -124,3 +134,48 @@ export type PartialOptions = Partial & { __resonate: true }; export function isOptions(o: unknown): o is PartialOptions { return typeof o === "object" && o !== null && (o as PartialOptions).__resonate === true; } + +export type StoreOptions = { + /** + * The store authentication options. + */ + auth: AuthOptions; + + /** + * The store encoder, defaults to a base64 encoder. + */ + encoder: IEncoder; + + /** + * The frequency in ms to heartbeat locks. + */ + heartbeat: number; + + /** + * A logger instance, if not provided a default logger will be + * used. + */ + logger: ILogger; + + /** + * A process id that can be used to uniquely identify this Resonate + * instance. If not provided a default value will be generated. + */ + pid: string; + + /** + * Number of retries to attempt before throwing an error. If not + * provided, a default value will be used. + */ + retries: number; +}; + +export type AuthOptions = { + /** + * Basic auth credentials. + */ + basic: { + password: string; + username: string; + }; +}; diff --git a/lib/core/stores/local.ts b/lib/core/stores/local.ts index 63eba97..7b9d3b8 100644 --- a/lib/core/stores/local.ts +++ b/lib/core/stores/local.ts @@ -2,6 +2,7 @@ import * as cronParser from "cron-parser"; import { ErrorCodes, ResonateError } from "../errors"; import { ILogger } from "../logger"; import { Logger } from "../loggers/logger"; +import { StoreOptions } from "../options"; import { DurablePromise, PendingPromise, @@ -26,18 +27,22 @@ export class LocalStore implements IStore { public schedules: LocalScheduleStore; public locks: LocalLockStore; + public readonly logger: ILogger; + private toSchedule: Schedule[] = []; private next: number | undefined = undefined; constructor( - private logger: ILogger = new Logger(), + opts: Partial = {}, promiseStorage: IStorage = new WithTimeout(new MemoryStorage()), scheduleStorage: IStorage = new MemoryStorage(), lockStorage: IStorage<{ id: string; eid: string }> = new MemoryStorage<{ id: string; eid: string }>(), ) { - this.promises = new LocalPromiseStore(promiseStorage); - this.schedules = new LocalScheduleStore(scheduleStorage, this); - this.locks = new LocalLockStore(lockStorage); + this.promises = new LocalPromiseStore(this, promiseStorage); + this.schedules = new LocalScheduleStore(this, scheduleStorage); + this.locks = new LocalLockStore(this, lockStorage); + + this.logger = opts.logger ?? new Logger(); this.init(); } @@ -115,7 +120,10 @@ export class LocalStore implements IStore { } export class LocalPromiseStore implements IPromiseStore { - constructor(private storage: IStorage = new MemoryStorage()) {} + constructor( + private store: LocalStore, + private storage: IStorage, + ) {} async create( id: string, @@ -327,8 +335,8 @@ export class LocalPromiseStore implements IPromiseStore { export class LocalScheduleStore implements IScheduleStore { constructor( - private storage: IStorage = new MemoryStorage(), - private store: LocalStore | undefined = undefined, + private store: LocalStore, + private storage: IStorage, ) {} async create( @@ -456,7 +464,8 @@ export class LocalScheduleStore implements IScheduleStore { export class LocalLockStore implements ILockStore { constructor( - private storage: IStorage<{ id: string; eid: string }> = new MemoryStorage<{ id: string; eid: string }>(), + private store: LocalStore, + private storage: IStorage<{ id: string; eid: string }>, ) {} async tryAcquire(id: string, eid: string): Promise { diff --git a/lib/core/stores/remote.ts b/lib/core/stores/remote.ts index 2fb0c02..863a18d 100644 --- a/lib/core/stores/remote.ts +++ b/lib/core/stores/remote.ts @@ -3,6 +3,7 @@ import { Base64Encoder } from "../encoders/base64"; import { ErrorCodes, ResonateError } from "../errors"; import { ILogger } from "../logger"; import { Logger } from "../loggers/logger"; +import { StoreOptions } from "../options"; import { DurablePromise, PendingPromise, @@ -15,31 +16,106 @@ import { } from "../promises/types"; import { Schedule, isSchedule } from "../schedules/types"; import { IStore, IPromiseStore, IScheduleStore, ILockStore } from "../store"; +import * as utils from "../utils"; export class RemoteStore implements IStore { - public promises: RemotePromiseStore; - public schedules: RemoteScheduleStore; - public locks: RemoteLockStore; + private readonly headers: Record = { + Accept: "application/json", + "Content-Type": "application/json", + }; + + public readonly promises: RemotePromiseStore; + public readonly schedules: RemoteScheduleStore; + public readonly locks: RemoteLockStore; + + public readonly encoder: IEncoder; + public readonly heartbeat: number; + public readonly logger: ILogger; + public readonly pid: string; + public readonly retries: number; constructor( - url: string, - pid: string, - logger: ILogger = new Logger(), - encoder: IEncoder = new Base64Encoder(), - heartbeatFrequency?: number, + public readonly url: string, + opts: Partial = {}, ) { - this.promises = new RemotePromiseStore(url, logger, encoder); - this.schedules = new RemoteScheduleStore(url, logger, encoder); - this.locks = new RemoteLockStore(url, pid, logger, heartbeatFrequency); + // store components + this.promises = new RemotePromiseStore(this); + this.schedules = new RemoteScheduleStore(this); + this.locks = new RemoteLockStore(this); + + // store options + this.encoder = opts.encoder ?? new Base64Encoder(); + this.logger = opts.logger ?? new Logger(); + this.heartbeat = opts.heartbeat ?? 15000; + this.pid = opts.pid ?? utils.randomId(); + this.retries = opts.retries ?? 2; + + // auth + if (opts.auth?.basic) { + this.headers["Authorization"] = `Basic ${btoa(`${opts.auth.basic.username}:${opts.auth.basic.password}`)}`; + } + } + + async call(path: string, guard: (b: unknown) => b is T, options: RequestInit): Promise { + let error: unknown; + + // add auth headers + options.headers = { ...this.headers, ...options.headers }; + + for (let i = 0; i < this.retries + 1; i++) { + try { + this.logger.debug("store:req", { + url: this.url, + method: options.method, + headers: options.headers, + body: options.body, + }); + + const r = await fetch(`${this.url}/${path}`, options); + const body: unknown = r.status !== 204 ? await r.json() : undefined; + + this.logger.debug("store:res", { + status: r.status, + body: body, + }); + + if (!r.ok) { + switch (r.status) { + case 400: + throw new ResonateError("Invalid request", ErrorCodes.STORE_PAYLOAD, body); + case 401: + throw new ResonateError("Unauthorized request", ErrorCodes.STORE_UNAUTHORIZED, body); + case 403: + throw new ResonateError("Forbidden request", ErrorCodes.STORE_FORBIDDEN, body); + case 404: + throw new ResonateError("Not found", ErrorCodes.STORE_NOT_FOUND, body); + case 409: + throw new ResonateError("Already exists", ErrorCodes.STORE_ALREADY_EXISTS, body); + default: + throw new ResonateError("Server error", ErrorCodes.STORE, body, true); + } + } + + if (!guard(body)) { + throw new ResonateError("Invalid response", ErrorCodes.STORE_PAYLOAD, body); + } + + return body; + } catch (e: unknown) { + if (e instanceof ResonateError && !e.retriable) { + throw e; + } else { + error = e; + } + } + } + + throw ResonateError.fromError(error); } } export class RemotePromiseStore implements IPromiseStore { - constructor( - private url: string, - private logger: ILogger = new Logger(), - private encoder: IEncoder = new Base64Encoder(), - ) {} + constructor(private store: RemoteStore) {} async create( id: string, @@ -51,8 +127,6 @@ export class RemotePromiseStore implements IPromiseStore { tags: Record | undefined, ): Promise { const reqHeaders: Record = { - Accept: "application/json", - "Content-Type": "application/json", Strict: JSON.stringify(strict), }; @@ -60,26 +134,21 @@ export class RemotePromiseStore implements IPromiseStore { reqHeaders["Idempotency-Key"] = ikey; } - const promise = await call( - `${this.url}/promises`, - isDurablePromise, - { - method: "POST", - headers: reqHeaders, - body: JSON.stringify({ - id: id, - param: { - headers: headers, - data: data ? encode(data, this.encoder) : undefined, - }, - timeout: timeout, - tags: tags, - }), - }, - this.logger, - ); + const promise = await this.store.call("promises", isDurablePromise, { + method: "POST", + headers: reqHeaders, + body: JSON.stringify({ + id: id, + param: { + headers: headers, + data: data ? encode(data, this.store.encoder) : undefined, + }, + timeout: timeout, + tags: tags, + }), + }); - return decode(promise, this.encoder); + return decode(promise, this.store.encoder); } async cancel( @@ -90,8 +159,6 @@ export class RemotePromiseStore implements IPromiseStore { data: string | undefined, ): Promise { const reqHeaders: Record = { - Accept: "application/json", - "Content-Type": "application/json", Strict: JSON.stringify(strict), }; @@ -99,24 +166,19 @@ export class RemotePromiseStore implements IPromiseStore { reqHeaders["Idempotency-Key"] = ikey; } - const promise = await call( - `${this.url}/promises/${id}`, - isCompletedPromise, - { - method: "PATCH", - headers: reqHeaders, - body: JSON.stringify({ - state: "REJECTED_CANCELED", - value: { - headers: headers, - data: data ? encode(data, this.encoder) : undefined, - }, - }), - }, - this.logger, - ); + const promise = await this.store.call(`promises/${id}`, isCompletedPromise, { + method: "PATCH", + headers: reqHeaders, + body: JSON.stringify({ + state: "REJECTED_CANCELED", + value: { + headers: headers, + data: data ? encode(data, this.store.encoder) : undefined, + }, + }), + }); - return decode(promise, this.encoder); + return decode(promise, this.store.encoder); } async resolve( @@ -127,8 +189,6 @@ export class RemotePromiseStore implements IPromiseStore { data: string | undefined, ): Promise { const reqHeaders: Record = { - Accept: "application/json", - "Content-Type": "application/json", Strict: JSON.stringify(strict), }; @@ -136,24 +196,19 @@ export class RemotePromiseStore implements IPromiseStore { reqHeaders["Idempotency-Key"] = ikey; } - const promise = await call( - `${this.url}/promises/${id}`, - isCompletedPromise, - { - method: "PATCH", - headers: reqHeaders, - body: JSON.stringify({ - state: "RESOLVED", - value: { - headers: headers, - data: data ? encode(data, this.encoder) : undefined, - }, - }), - }, - this.logger, - ); + const promise = await this.store.call(`promises/${id}`, isCompletedPromise, { + method: "PATCH", + headers: reqHeaders, + body: JSON.stringify({ + state: "RESOLVED", + value: { + headers: headers, + data: data ? encode(data, this.store.encoder) : undefined, + }, + }), + }); - return decode(promise, this.encoder); + return decode(promise, this.store.encoder); } async reject( @@ -164,8 +219,6 @@ export class RemotePromiseStore implements IPromiseStore { data: string | undefined, ): Promise { const reqHeaders: Record = { - Accept: "application/json", - "Content-Type": "application/json", Strict: JSON.stringify(strict), }; @@ -173,41 +226,27 @@ export class RemotePromiseStore implements IPromiseStore { reqHeaders["Idempotency-Key"] = ikey; } - const promise = await call( - `${this.url}/promises/${id}`, - isCompletedPromise, - { - method: "PATCH", - headers: reqHeaders, - body: JSON.stringify({ - state: "REJECTED", - value: { - headers: headers, - data: data ? encode(data, this.encoder) : undefined, - }, - }), - }, - this.logger, - ); + const promise = await this.store.call(`promises/${id}`, isCompletedPromise, { + method: "PATCH", + headers: reqHeaders, + body: JSON.stringify({ + state: "REJECTED", + value: { + headers: headers, + data: data ? encode(data, this.store.encoder) : undefined, + }, + }), + }); - return decode(promise, this.encoder); + return decode(promise, this.store.encoder); } async get(id: string): Promise { - const promise = await call( - `${this.url}/promises/${id}`, - isDurablePromise, - { - method: "GET", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - }, - this.logger, - ); + const promise = await this.store.call(`promises/${id}`, isDurablePromise, { + method: "GET", + }); - return decode(promise, this.encoder); + return decode(promise, this.store.encoder); } async *search( @@ -237,34 +276,18 @@ export class RemotePromiseStore implements IPromiseStore { params.append("cursor", cursor); } - const url = new URL(`${this.url}/promises`); - url.search = params.toString(); - - const res = await call( - url.toString(), - isSearchPromiseResult, - { - method: "GET", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - }, - this.logger, - ); + const res = await this.store.call(`promises?${params.toString()}`, isSearchPromiseResult, { + method: "GET", + }); cursor = res.cursor; - yield res.promises.map((p) => decode(p, this.encoder)); + yield res.promises.map((p) => decode(p, this.store.encoder)); } } } export class RemoteScheduleStore implements IScheduleStore { - constructor( - private url: string, - private logger: ILogger = new Logger(), - private encoder: IEncoder = new Base64Encoder(), - ) {} + constructor(private store: RemoteStore) {} async create( id: string, @@ -278,71 +301,45 @@ export class RemoteScheduleStore implements IScheduleStore { promiseData: string | undefined, promiseTags: Record | undefined, ): Promise { - const reqHeaders: Record = { - Accept: "application/json", - "Content-Type": "application/json", - }; + const reqHeaders: Record = {}; if (ikey !== undefined) { reqHeaders["Idempotency-Key"] = ikey; } - const schedule = call( - `${this.url}/schedules`, - isSchedule, - { - method: "POST", - headers: reqHeaders, - body: JSON.stringify({ - id, - description, - cron, - tags, - promiseId, - promiseTimeout, - promiseParam: { - headers: promiseHeaders, - data: promiseData ? encode(promiseData, this.encoder) : undefined, - }, - promiseTags, - }), - }, - this.logger, - ); + const schedule = this.store.call(`schedules`, isSchedule, { + method: "POST", + headers: reqHeaders, + body: JSON.stringify({ + id, + description, + cron, + tags, + promiseId, + promiseTimeout, + promiseParam: { + headers: promiseHeaders, + data: promiseData ? encode(promiseData, this.store.encoder) : undefined, + }, + promiseTags, + }), + }); return schedule; } async get(id: string): Promise { - const schedule = call( - `${this.url}/schedules/${id}`, - isSchedule, - { - method: "GET", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - }, - this.logger, - ); + const schedule = this.store.call(`schedules/${id}`, isSchedule, { + method: "GET", + }); return schedule; } async delete(id: string): Promise { - await call( - `${this.url}/schedules/${id}`, - (b: unknown): b is any => true, - { - method: "DELETE", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - }, - this.logger, - ); + await this.store.call(`schedules/${id}`, (b: unknown): b is any => true, { + method: "DELETE", + }); } async *search( @@ -367,21 +364,9 @@ export class RemoteScheduleStore implements IScheduleStore { params.append("cursor", cursor); } - const url = new URL(`${this.url}/schedules`); - url.search = params.toString(); - - const res = await call( - url.toString(), - isSearchSchedulesResult, - { - method: "GET", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - }, - this.logger, - ); + const res = await this.store.call(`schedules?${params.toString()}`, isSearchSchedulesResult, { + method: "GET", + }); cursor = res.cursor; yield res.schedules; @@ -393,35 +378,25 @@ export class RemoteLockStore implements ILockStore { private heartbeatInterval: number | null = null; private locksHeld: number = 0; - constructor( - private url: string, - private pid: string, - private logger: ILogger = new Logger(), - private heartbeatFrequency: number = 15000, - ) {} + constructor(private store: RemoteStore) {} async tryAcquire( resourceId: string, executionId: string, - expiry: number = this.heartbeatFrequency * 4, + expiry: number = this.store.heartbeat * 4, ): Promise { // lock expiry cannot be less than heartbeat frequency - expiry = Math.max(expiry, this.heartbeatFrequency); - - await call( - `${this.url}/locks/acquire`, - (b: unknown): b is any => true, - { - method: "POST", - body: JSON.stringify({ - resourceId: resourceId, - processId: this.pid, - executionId: executionId, - expiryInMilliseconds: expiry, - }), - }, - this.logger, - ); + expiry = Math.max(expiry, this.store.heartbeat); + + await this.store.call(`locks/acquire`, (b: unknown): b is any => true, { + method: "POST", + body: JSON.stringify({ + resourceId: resourceId, + processId: this.store.pid, + executionId: executionId, + expiryInMilliseconds: expiry, + }), + }); // increment the number of locks held this.locksHeld++; @@ -433,21 +408,13 @@ export class RemoteLockStore implements ILockStore { } async release(resourceId: string, executionId: string): Promise { - await call( - `${this.url}/locks/release`, - (b: unknown): b is void => b === undefined, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - resourceId, - executionId, - }), - }, - this.logger, - ); + await this.store.call(`locks/release`, (b: unknown): b is void => b === undefined, { + method: "POST", + body: JSON.stringify({ + resourceId, + executionId, + }), + }); // decrement the number of locks held this.locksHeld = Math.max(this.locksHeld - 1, 0); @@ -462,7 +429,7 @@ export class RemoteLockStore implements ILockStore { private startHeartbeat(): void { if (this.heartbeatInterval === null) { // the + converts to a number - this.heartbeatInterval = +setInterval(() => this.heartbeat(), this.heartbeatFrequency); + this.heartbeatInterval = +setInterval(() => this.heartbeat(), this.store.heartbeat); } } @@ -474,20 +441,16 @@ export class RemoteLockStore implements ILockStore { } private async heartbeat() { - const res = await call<{ locksAffected: number }>( - `${this.url}/locks/heartbeat`, + const res = await this.store.call<{ locksAffected: number }>( + `locks/heartbeat`, (b: unknown): b is { locksAffected: number } => typeof b === "object" && b !== null && "locksAffected" in b && typeof b.locksAffected === "number", { method: "POST", - headers: { - "Content-Type": "application/json", - }, body: JSON.stringify({ - processId: this.pid, + processId: this.store.pid, }), }, - this.logger, ); // set the number of locks held @@ -501,64 +464,6 @@ export class RemoteLockStore implements ILockStore { // Utils -async function call( - url: string, - guard: (b: unknown) => b is T, - options: RequestInit, - logger: ILogger, - retries: number = 3, -): Promise { - let error: unknown; - - for (let i = 0; i < retries; i++) { - try { - logger.debug("store:req", { - method: options.method, - url: url, - headers: options.headers, - body: options.body, - }); - - const r = await fetch(url, options); - const body: unknown = r.status !== 204 ? await r.json() : undefined; - - logger.debug("store:res", { - status: r.status, - body: body, - }); - - if (!r.ok) { - switch (r.status) { - case 400: - throw new ResonateError("Invalid request", ErrorCodes.STORE_PAYLOAD, body); - case 403: - throw new ResonateError("Forbidden request", ErrorCodes.STORE_FORBIDDEN, body); - case 404: - throw new ResonateError("Not found", ErrorCodes.STORE_NOT_FOUND, body); - case 409: - throw new ResonateError("Already exists", ErrorCodes.STORE_ALREADY_EXISTS, body); - default: - throw new ResonateError("Server error", ErrorCodes.STORE, body, true); - } - } - - if (!guard(body)) { - throw new ResonateError("Invalid response", ErrorCodes.STORE_PAYLOAD, body); - } - - return body; - } catch (e: unknown) { - if (e instanceof ResonateError && !e.retriable) { - throw e; - } else { - error = e; - } - } - } - - throw ResonateError.fromError(error); -} - function encode(value: string, encoder: IEncoder): string { try { return encoder.encode(value); diff --git a/lib/resonate.ts b/lib/resonate.ts index bd7ba75..65a34ed 100644 --- a/lib/resonate.ts +++ b/lib/resonate.ts @@ -42,7 +42,9 @@ export abstract class ResonateBase { private interval: NodeJS.Timeout | undefined; constructor({ + auth = undefined, encoder = new JSONEncoder(), + heartbeat = 15000, // 15s logger = new Logger(), pid = utils.randomId(), poll = 5000, // 5s @@ -63,9 +65,19 @@ export abstract class ResonateBase { if (store) { this.store = store; } else if (url) { - this.store = new RemoteStore(url, this.pid, this.logger); + this.store = new RemoteStore(url, { + auth, + heartbeat, + logger, + pid, + }); } else { - this.store = new LocalStore(this.logger); + this.store = new LocalStore({ + auth, + heartbeat, + logger, + pid, + }); } // promises diff --git a/test/auth.test.ts b/test/auth.test.ts new file mode 100644 index 0000000..d9d81ae --- /dev/null +++ b/test/auth.test.ts @@ -0,0 +1,59 @@ +import { describe, test, expect, jest, beforeEach } from "@jest/globals"; +import { RemoteStore } from "../lib"; + +jest.setTimeout(10000); + +describe("Auth", () => { + // mock fetch + // return a 500 so that an error is thrown (and ignored) + const fetchMock = jest.fn(async () => new Response(null, { status: 500 })); + global.fetch = fetchMock; + + const store = new RemoteStore("http://localhost:8080", { + auth: { basic: { username: "foo", password: "bar" } }, + retries: 0, + }); + + // prettier-ignore + const funcs = [ + // promises + { name: "promises.get", func: () => store.promises.get("") }, + { name: "promises.create", func: () => store.promises.create("", undefined, false, undefined, undefined, 0, undefined) }, + { name: "promises.cancel", func: () => store.promises.cancel("", undefined, false, undefined, undefined) }, + { name: "promises.resolve", func: () => store.promises.resolve("", undefined, false, undefined, undefined) }, + { name: "promises.reject", func: () => store.promises.reject("", undefined, false, undefined, undefined) }, + { name: "promises.search", func: () => store.promises.search("", undefined, undefined, undefined).next() }, + + // schedules + { name: "schedules.get", func: () => store.schedules.get("") }, + { name: "schedules.create", func: () => store.schedules.create("", undefined, undefined, "", undefined, "", 0, undefined, undefined, undefined) }, + { name: "schedules.delete", func: () => store.schedules.delete("") }, + { name: "schedules.search", func: () => store.schedules.search("", undefined, undefined).next() }, + + // locks + { name: "locks.tryAcquire", func: () => store.locks.tryAcquire("", "") }, + { name: "locks.release", func: () => store.locks.release("", "") }, + ]; + + beforeEach(() => { + fetchMock.mockClear(); + }); + + describe("basic auth", () => { + for (const { name, func } of funcs) { + test(name, async () => { + await func().catch(() => {}); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Basic Zm9vOmJhcg==`, + }), + }), + ); + }); + } + }); +}); diff --git a/test/promiseTransitions.test.ts b/test/promiseTransitions.test.ts index ba47eb4..7fb8450 100644 --- a/test/promiseTransitions.test.ts +++ b/test/promiseTransitions.test.ts @@ -1,21 +1,20 @@ import { jest, describe, test, expect } from "@jest/globals"; -import { WithTimeout } from "../lib/core/storages/withTimeout"; -import { IPromiseStore } from "../lib/core/store"; -import { LocalPromiseStore } from "../lib/core/stores/local"; -import { RemotePromiseStore } from "../lib/core/stores/remote"; +import { IStore } from "../lib/core/store"; +import { LocalStore } from "../lib/core/stores/local"; +import { RemoteStore } from "../lib/core/stores/remote"; // Set a larger timeout for hooks (e.g., 10 seconds) jest.setTimeout(10000); describe("Store: Promise Transitions", () => { - const stores: IPromiseStore[] = [new LocalPromiseStore(new WithTimeout())]; + const stores: IStore[] = [new LocalStore()]; if (process.env.RESONATE_STORE_URL) { - stores.push(new RemotePromiseStore(process.env.RESONATE_STORE_URL)); + stores.push(new RemoteStore(process.env.RESONATE_STORE_URL)); } - for (const store of stores) { + for (const store of stores.map((s) => s.promises)) { describe(store.constructor.name, () => { test("Test Case 0: transitions from Init to Pending via Create", async () => { const promise = await store.create( diff --git a/test/promises.test.ts b/test/promises.test.ts index 41f98cc..c6702c4 100644 --- a/test/promises.test.ts +++ b/test/promises.test.ts @@ -1,20 +1,19 @@ import { jest, describe, test, expect } from "@jest/globals"; -import { WithTimeout } from "../lib/core/storages/withTimeout"; -import { IPromiseStore } from "../lib/core/store"; -import { LocalPromiseStore } from "../lib/core/stores/local"; -import { RemotePromiseStore } from "../lib/core/stores/remote"; +import { IStore } from "../lib/core/store"; +import { LocalStore } from "../lib/core/stores/local"; +import { RemoteStore } from "../lib/core/stores/remote"; jest.setTimeout(10000); describe("Store: Promise", () => { - const stores: IPromiseStore[] = [new LocalPromiseStore(new WithTimeout())]; + const stores: IStore[] = [new LocalStore()]; if (process.env.RESONATE_STORE_URL) { - stores.push(new RemotePromiseStore(process.env.RESONATE_STORE_URL)); + stores.push(new RemoteStore(process.env.RESONATE_STORE_URL)); } - for (const store of stores) { + for (const store of stores.map((s) => s.promises)) { describe(store.constructor.name, () => { test("Promise Store: Get promise that exists", async () => { const promiseId = "existing-promise"; diff --git a/test/schedules.test.ts b/test/schedules.test.ts index b029e0d..4bd54bc 100644 --- a/test/schedules.test.ts +++ b/test/schedules.test.ts @@ -1,19 +1,19 @@ import { describe, test, expect, jest } from "@jest/globals"; import { Schedule } from "../lib/core/schedules/types"; -import { IScheduleStore } from "../lib/core/store"; -import { LocalScheduleStore } from "../lib/core/stores/local"; -import { RemoteScheduleStore } from "../lib/core/stores/remote"; +import { IStore } from "../lib/core/store"; +import { LocalStore } from "../lib/core/stores/local"; +import { RemoteStore } from "../lib/core/stores/remote"; jest.setTimeout(10000); describe("Store: Schedules", () => { - const stores: IScheduleStore[] = [new LocalScheduleStore()]; + const stores: IStore[] = [new LocalStore()]; if (process.env.RESONATE_STORE_URL) { - stores.push(new RemoteScheduleStore(process.env.RESONATE_STORE_URL)); + stores.push(new RemoteStore(process.env.RESONATE_STORE_URL)); } - for (const store of stores) { + for (const store of stores.map((s) => s.schedules)) { describe(store.constructor.name, () => { test("Schedule Store: Create schedule", async () => { const scheduleId = "new-schedule";