mirror of
https://github.com/appium/appium.git
synced 2026-02-20 18:30:11 -06:00
feat(strongbox): create @appium/strongbox
This PR creates a new package `@appium/strongbox`, which provides a generic persistence store for Appium extensions.
This commit is contained in:
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@@ -147,3 +147,8 @@ labels:
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/universal-xml-plugin/**']
|
||||
|
||||
- label: '@appium/strongbox'
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/strongbox/**']
|
||||
|
||||
34
package-lock.json
generated
34
package-lock.json
generated
@@ -139,6 +139,10 @@
|
||||
"resolved": "packages/schema",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@appium/strongbox": {
|
||||
"resolved": "packages/strongbox",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@appium/support": {
|
||||
"resolved": "packages/support",
|
||||
"link": true
|
||||
@@ -8018,8 +8022,8 @@
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -17598,6 +17602,14 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/slugify": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
|
||||
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"dev": true,
|
||||
@@ -20264,6 +20276,16 @@
|
||||
"npm": ">=8"
|
||||
}
|
||||
},
|
||||
"packages/clerk": {
|
||||
"name": "@appium/clerk",
|
||||
"version": "0.1.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"env-paths": "2.2.1",
|
||||
"slugify": "1.6.6"
|
||||
}
|
||||
},
|
||||
"packages/doctor": {
|
||||
"name": "@appium/doctor",
|
||||
"version": "2.0.11",
|
||||
@@ -20623,6 +20645,14 @@
|
||||
"npm": ">=8"
|
||||
}
|
||||
},
|
||||
"packages/strongbox": {
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"env-paths": "2.2.1",
|
||||
"slugify": "1.6.6"
|
||||
}
|
||||
},
|
||||
"packages/support": {
|
||||
"name": "@appium/support",
|
||||
"version": "3.1.9",
|
||||
|
||||
96
packages/strongbox/README.md
Normal file
96
packages/strongbox/README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# @appium/strongbox
|
||||
|
||||
> Persistent storage for Appium extensions
|
||||
|
||||
## Summary
|
||||
|
||||
This package is intended to be used in [Appium](https://appium.io) extensions which need to persist data between Appium runs. An example of such data may be a device token or key.
|
||||
|
||||
`@appium/strongbox` provides a simple and extensible API for managing such data, while abstracting the underlying storage mechanism.
|
||||
|
||||
_Note:_ This module is not intended for storing sensitive data.
|
||||
|
||||
## Usage
|
||||
|
||||
First, create an instance of `Strongbox`:
|
||||
|
||||
```ts
|
||||
import {strongbox} from '@appium/strongbox';
|
||||
|
||||
const box = strongbox('my-pkg');
|
||||
```
|
||||
|
||||
This instance corresponds to a unique collection of data.
|
||||
|
||||
From here, create a placeholder for data (you will need to provide the type of data you intend to store):
|
||||
|
||||
```ts
|
||||
const item = await box.createItem<string>('my unique name');
|
||||
```
|
||||
|
||||
...or, if you already have the data on-hand:
|
||||
|
||||
```ts
|
||||
const item: Buffer|string = getSomeData();
|
||||
|
||||
const item = await box.createItemWithContents('my unique name', data);
|
||||
```
|
||||
|
||||
Either way, you can read its contents:
|
||||
|
||||
```ts
|
||||
// if the item doesn't exist, this result will be undefined
|
||||
const contents = await item.read();
|
||||
```
|
||||
|
||||
Or write new data to the item:
|
||||
|
||||
```ts
|
||||
await item.write('new stuff');
|
||||
```
|
||||
|
||||
The last-read contents of the `Item` will be available on the `contents` property, but the value of this property is only current as of the last `read()`:
|
||||
|
||||
```ts
|
||||
const {contents} = item;
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
In lieu of actual documentation, look at the type definitions that this package ships.
|
||||
|
||||
## Customization
|
||||
|
||||
1. Create a class that implements the `Item` interface:
|
||||
|
||||
```ts
|
||||
import {strongbox, Item} from '@appium/strongbox';
|
||||
import {Foo, getFoo} from 'somewhere/else';
|
||||
|
||||
class FooItem implements Item<Foo> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
2. Provide this class as the `defaultCtor` option to `strongbox()`:
|
||||
|
||||
```ts
|
||||
const box = strongbox('my-pkg', {defaultCtor: FooItem});
|
||||
```
|
||||
|
||||
3. Use like you would any other `Strongbox` instance:
|
||||
|
||||
```ts
|
||||
const foo: Foo = getFoo();
|
||||
const item = await box.createItemWithValue('my unique name', Foo);
|
||||
```
|
||||
|
||||
## Default Behavior, For the Curious
|
||||
|
||||
Out-of-the-box, a `Strongbox` instance corresponds to a directory on-disk, and each `Item` (returned by `createItem()/createItemWithContents()`) corresponds to a file within that directory.
|
||||
|
||||
The directory of the `Strongbox` instance is determined by the [env-paths](https://www.npmjs.com/package/env-paths) package, and is platform-specific.
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2023 OpenJS Foundation. Licensed Apache-2.0
|
||||
95
packages/strongbox/lib/base-item.ts
Normal file
95
packages/strongbox/lib/base-item.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {mkdir, readFile, unlink, writeFile} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type {Item, ItemEncoding, Value} from '.';
|
||||
import {slugify} from './util';
|
||||
|
||||
/**
|
||||
* Base item implementation
|
||||
*
|
||||
* @remarks This class is not intended to be instantiated directly
|
||||
* @typeParam T - Type of data stored in the `Item`
|
||||
*/
|
||||
export class BaseItem<T extends Value> implements Item<T> {
|
||||
/**
|
||||
* {@inheritdoc Item.value}
|
||||
*/
|
||||
protected _value?: T | undefined;
|
||||
|
||||
/**
|
||||
* Unique slugified identifier
|
||||
*/
|
||||
public readonly id: string;
|
||||
|
||||
/**
|
||||
* {@inheritdoc Item.value}
|
||||
*/
|
||||
public readonly value: T | undefined;
|
||||
|
||||
/**
|
||||
* Slugifies the name
|
||||
* @param name Name of instance
|
||||
* @param container Slugified name of container
|
||||
* @param encoding Defaults to `utf8`
|
||||
*/
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly container: string,
|
||||
public readonly encoding: ItemEncoding = 'utf8'
|
||||
) {
|
||||
this.id = path.join(container, slugify(name));
|
||||
|
||||
Object.defineProperties(this, {
|
||||
value: {
|
||||
get() {
|
||||
return this._value;
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
_value: {
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Item.read}
|
||||
*/
|
||||
public async read(): Promise<T | undefined> {
|
||||
try {
|
||||
this._value = (await readFile(this.id, {
|
||||
encoding: this.encoding,
|
||||
})) as T;
|
||||
} catch (e) {
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return this._value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Item.write}
|
||||
*/
|
||||
public async write(value: T): Promise<void> {
|
||||
if (this._value !== value) {
|
||||
await mkdir(path.dirname(this.id), {recursive: true});
|
||||
await writeFile(this.id, value, this.encoding);
|
||||
this._value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Item.clear}
|
||||
*/
|
||||
public async clear(): Promise<void> {
|
||||
try {
|
||||
await unlink(this.id);
|
||||
this._value = undefined;
|
||||
} catch (e) {
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
284
packages/strongbox/lib/index.ts
Normal file
284
packages/strongbox/lib/index.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import envPaths from 'env-paths';
|
||||
import path from 'node:path';
|
||||
import {BaseItem} from './base-item';
|
||||
import {slugify, slugifyPath} from './util';
|
||||
|
||||
/**
|
||||
* Valid file encodings.
|
||||
*
|
||||
* `null` means the file should be read and written as a `Buffer`.
|
||||
*/
|
||||
export type ItemEncoding = BufferEncoding | null;
|
||||
|
||||
/**
|
||||
* Valid value wrapped by {@linkcode Item}. Can be an encoded string or `Buffer`
|
||||
*/
|
||||
export type Value = string | Buffer;
|
||||
|
||||
/**
|
||||
* An object representing a persisted item containing something of type `T` (which can be a {@linkcode Buffer} or an encoded string; see {@linkcode ItemEncoding}).
|
||||
*
|
||||
* A {@linkcode Item} does not know anything about where it is stored, or how it is stored.
|
||||
* @typeParam T - Type of data stored in the item
|
||||
*/
|
||||
export interface Item<T extends Value> {
|
||||
/**
|
||||
* Encoding of underlying value
|
||||
*/
|
||||
encoding: ItemEncoding;
|
||||
/**
|
||||
* Slugified name
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Name of item
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Reads value
|
||||
*/
|
||||
read(): Promise<T | undefined>;
|
||||
/**
|
||||
* Writes value
|
||||
* @param value New value
|
||||
*/
|
||||
write(value: T): Promise<void>;
|
||||
|
||||
/**
|
||||
* Deletes the item.
|
||||
*/
|
||||
clear(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Last known value (stored in memory)
|
||||
*
|
||||
* @remarks A custom {@linkcode Item} meant to handle very large files should probably not implement this.
|
||||
*/
|
||||
value?: T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of known `Item` encodings
|
||||
* @internal
|
||||
*/
|
||||
const ITEM_ENCODINGS: Readonly<Set<ItemEncoding>> = new Set([
|
||||
'ascii',
|
||||
'utf8',
|
||||
'utf-8',
|
||||
'utf16le',
|
||||
'ucs2',
|
||||
'ucs-2',
|
||||
'base64',
|
||||
'base64url',
|
||||
'latin1',
|
||||
'binary',
|
||||
'hex',
|
||||
null,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Type guard for encodings
|
||||
* @param value any
|
||||
* @returns `true` is `value` is a valid file encoding
|
||||
*/
|
||||
function isEncoding(value: any): value is ItemEncoding {
|
||||
return ITEM_ENCODINGS.has(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@linkcode StrongboxOpts}
|
||||
*/
|
||||
export const DEFAULT_SUFFIX = 'strongbox';
|
||||
|
||||
/**
|
||||
* A constructor function which instantiates a {@linkcode Item}.
|
||||
*/
|
||||
export type ItemCtor<T extends Value> = new (
|
||||
name: string,
|
||||
container: string,
|
||||
encoding?: ItemEncoding
|
||||
) => Item<T>;
|
||||
|
||||
/**
|
||||
* Main entry point for use of this module
|
||||
*
|
||||
* Manages multiple {@linkcode Item}s.
|
||||
*/
|
||||
export class Strongbox {
|
||||
/**
|
||||
* Default {@linkcode ItemCtor} to use when creating new {@linkcode Item}s
|
||||
*/
|
||||
protected defaultItemCtor: ItemCtor<any>;
|
||||
/**
|
||||
* Slugified name of this instance; corresponds to the directory name.
|
||||
*
|
||||
* If `dir` is provided, this value is unused.
|
||||
* If `suffix` is provided, then this will be the parent directory of `suffix`.
|
||||
*/
|
||||
public readonly id: string;
|
||||
|
||||
/**
|
||||
* Override the directory of this container.
|
||||
*
|
||||
* If this is present, both `suffix` and `containerId` are unused.
|
||||
*/
|
||||
public readonly container: string;
|
||||
|
||||
/**
|
||||
* Store of known {@linkcode Item}s
|
||||
* @internal
|
||||
*/
|
||||
protected items: Map<string, WeakRef<Item<any>>>;
|
||||
|
||||
/**
|
||||
* Slugifies the name & determines the directory
|
||||
* @param name Name of instance
|
||||
* @param opts Options
|
||||
*/
|
||||
protected constructor(
|
||||
public readonly name: string,
|
||||
{container, suffix = DEFAULT_SUFFIX, defaultItemCtor: defaultCtor = BaseItem}: StrongboxOpts = {}
|
||||
) {
|
||||
this.id = slugify(name);
|
||||
this.defaultItemCtor = defaultCtor;
|
||||
this.items = new Map();
|
||||
this.container = container
|
||||
? slugifyPath(container)
|
||||
: path.join(envPaths(this.id).data, slugify(suffix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@linkcode Strongbox}
|
||||
* @param name Name of instance
|
||||
* @param opts Options
|
||||
* @returns New instance
|
||||
*/
|
||||
public static create(name: string, opts?: StrongboxOpts) {
|
||||
return new Strongbox(name, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@linkcode Item}.
|
||||
*
|
||||
* Reads the item, if it is already persisted. Does not throw if missing.
|
||||
* @param name Unique name of item
|
||||
* @param encoding Encoding of item; defaults to `utf8`
|
||||
* @returns New `Item`
|
||||
* @typeParam T - Type of data stored in the `Item`
|
||||
*/
|
||||
async createItem<T extends Value>(
|
||||
name: string,
|
||||
ctor?: ItemCtor<T>,
|
||||
encoding?: ItemEncoding
|
||||
): Promise<Item<T>>;
|
||||
async createItem<T extends Value>(name: string, encoding?: ItemEncoding): Promise<Item<T>>;
|
||||
async createItem<T extends Value>(
|
||||
name: string,
|
||||
encodingOrCtor?: ItemEncoding | ItemCtor<T>,
|
||||
encoding?: ItemEncoding
|
||||
): Promise<Item<T>> {
|
||||
if (isEncoding(encodingOrCtor)) {
|
||||
encoding = encodingOrCtor;
|
||||
encodingOrCtor = this.defaultItemCtor;
|
||||
}
|
||||
const item = new (encodingOrCtor ?? (this.defaultItemCtor as ItemCtor<T>))(
|
||||
name,
|
||||
this.container,
|
||||
encoding
|
||||
);
|
||||
if (this.items.has(item.id)) {
|
||||
throw new ReferenceError(`Item with id "${item.id}" already exists`);
|
||||
}
|
||||
try {
|
||||
await item.read();
|
||||
} catch (e) {
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.items.set(item.id, new WeakRef(item));
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@linkcode Item} then immediately writes value to it.
|
||||
*
|
||||
* If it exists already, it will be overwritten.
|
||||
* @param name Name of `Item`
|
||||
* @param value File value to write
|
||||
* @param ctor Specific {@linkcode ItemCtor} to use
|
||||
* @param encoding File encoding
|
||||
* @returns New `Item` w/ value of `value`
|
||||
*/
|
||||
async createItemWithValue<T extends Value>(
|
||||
name: string,
|
||||
value: T,
|
||||
ctor: ItemCtor<T>,
|
||||
encoding?: ItemEncoding
|
||||
): Promise<Item<T>>;
|
||||
async createItemWithValue<T extends Value>(
|
||||
name: string,
|
||||
value: T,
|
||||
encoding?: ItemEncoding
|
||||
): Promise<Item<T>>;
|
||||
async createItemWithValue<T extends Value>(
|
||||
name: string,
|
||||
value: T,
|
||||
encodingOrCtor?: ItemEncoding | ItemCtor<T>,
|
||||
encoding?: ItemEncoding
|
||||
): Promise<Item<T>> {
|
||||
const item = isEncoding(encodingOrCtor)
|
||||
? await this.createItem<T>(name, encodingOrCtor)
|
||||
: await this.createItem<T>(name, encodingOrCtor, encoding);
|
||||
await item.write(value);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to retrieve an {@linkcode Item} by its `id`.
|
||||
* @param id ID of item
|
||||
* @returns An `Item`, if found
|
||||
*/
|
||||
public getItem(id: string): Item<any> | undefined {
|
||||
const ref = this.items.get(id);
|
||||
return ref?.deref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears _all_ items, as well as the container.
|
||||
*/
|
||||
public async clearAll(): Promise<void> {
|
||||
const items = [...this.items.values()].map((ref) => ref.deref()).filter(Boolean) as Item<any>[];
|
||||
await Promise.all(items.map((item) => item.clear()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@linkcode strongbox}
|
||||
*/
|
||||
export interface StrongboxOpts {
|
||||
/**
|
||||
* Default {@linkcode Item} constructor.
|
||||
*
|
||||
* Unless a constructor is specified when calling {@linkcode Strongbox.createItem} or {@linkcode Strongbox.createItemWithValue}, this will be used.
|
||||
* @defaultValue BaseItem
|
||||
*/
|
||||
defaultItemCtor?: ItemCtor<any>;
|
||||
/**
|
||||
* Override default container, which is chosen according to environment
|
||||
*/
|
||||
container?: string;
|
||||
/**
|
||||
* Extra subdir to append to the auto-generated file directory hierarchy.
|
||||
*
|
||||
* This is ignored if `container` is provided.
|
||||
* @defaultValue 'strongbox'
|
||||
*/
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Strongbox.create}
|
||||
*/
|
||||
export const strongbox = Strongbox.create;
|
||||
10
packages/strongbox/lib/util.ts
Normal file
10
packages/strongbox/lib/util.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import path from 'node:path';
|
||||
import slug from 'slugify';
|
||||
|
||||
export function slugify(value: string) {
|
||||
return slug(value, {lower: true});
|
||||
}
|
||||
|
||||
export function slugifyPath(filepath: string) {
|
||||
return filepath.split(path.sep).map(slugify).join(path.sep);
|
||||
}
|
||||
52
packages/strongbox/package.json
Normal file
52
packages/strongbox/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@appium/strongbox",
|
||||
"version": "0.1.0",
|
||||
"description": "Persistent storage for Appium extensions",
|
||||
"keywords": [
|
||||
"automation",
|
||||
"javascript",
|
||||
"selenium",
|
||||
"webdriver",
|
||||
"ios",
|
||||
"android",
|
||||
"firefoxos",
|
||||
"testing"
|
||||
],
|
||||
"homepage": "https://appium.io",
|
||||
"bugs": {
|
||||
"url": "https://github.com/appium/appium/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appium/appium.git",
|
||||
"directory": "packages/strongbox"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "Appium Contributors",
|
||||
"main": "./build/lib/index.js",
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"build/lib",
|
||||
"lib",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run test:unit",
|
||||
"test:smoke": "node .",
|
||||
"test:unit": "mocha \"test/unit/**/*.spec.ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"env-paths": "2.2.1",
|
||||
"slugify": "1.6.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
156
packages/strongbox/test/unit/strongbox.spec.ts
Normal file
156
packages/strongbox/test/unit/strongbox.spec.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import path from 'node:path';
|
||||
import rewiremock from 'rewiremock/node';
|
||||
import type {Strongbox as TStrongbox, StrongboxOpts} from '../../lib';
|
||||
import {createSandbox, SinonSandbox, SinonStubbedMember} from 'sinon';
|
||||
import type fs from 'node:fs/promises';
|
||||
|
||||
const {expect} = chai;
|
||||
type MockFs = {
|
||||
[K in keyof typeof fs]: SinonStubbedMember<(typeof fs)[K]>;
|
||||
};
|
||||
|
||||
describe('Strongbox', function () {
|
||||
let strongbox: (name: string, opts?: StrongboxOpts) => TStrongbox;
|
||||
let Strongbox: new (name: string, opts?: StrongboxOpts) => TStrongbox;
|
||||
let sandbox: SinonSandbox;
|
||||
let DEFAULT_SUFFIX: string;
|
||||
let MockFs: MockFs = {} as any;
|
||||
|
||||
const DATA_DIR = '/some/dir';
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = createSandbox();
|
||||
({strongbox, DEFAULT_SUFFIX, Strongbox} = rewiremock.proxy(() => require('../../lib'), (r) => ({
|
||||
// all of these props are async functions
|
||||
'node:fs/promises': r
|
||||
.mockThrough((prop) => {
|
||||
MockFs = {...MockFs, [prop]: sandbox.stub().resolves()};
|
||||
return MockFs[prop as keyof typeof fs];
|
||||
})
|
||||
.dynamic(), // this allows us to change the mock behavior on-the-fly
|
||||
'env-paths': sandbox.stub().returns({data: DATA_DIR}),
|
||||
})));
|
||||
});
|
||||
|
||||
describe('static method', function () {
|
||||
describe('create()', function () {
|
||||
it('should return a new Strongbox', function () {
|
||||
const box = strongbox('test');
|
||||
expect(box).to.be.an.instanceOf(Strongbox);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('instance method', function () {
|
||||
let box: TStrongbox;
|
||||
|
||||
beforeEach(function () {
|
||||
box = strongbox('test');
|
||||
});
|
||||
|
||||
describe('createItem()', function () {
|
||||
describe('when a Item with the same id does not exist', function () {
|
||||
describe('when the file does not exist', function () {
|
||||
it('should create an empty Item', async function () {
|
||||
const item = await box.createItem('SLUG test');
|
||||
expect(item).to.eql({
|
||||
id: '/some/dir/strongbox/slug-test',
|
||||
name: 'SLUG test',
|
||||
encoding: 'utf8',
|
||||
value: undefined,
|
||||
container: '/some/dir/strongbox',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the file exists', function () {
|
||||
beforeEach(function () {
|
||||
MockFs.readFile.resolves('foo bar');
|
||||
});
|
||||
it('should read its value', async function () {
|
||||
const item = await box.createItem('SLUG test');
|
||||
expect(item).to.eql({
|
||||
id: '/some/dir/strongbox/slug-test',
|
||||
name: 'SLUG test',
|
||||
encoding: 'utf8',
|
||||
value: 'foo bar',
|
||||
container: '/some/dir/strongbox',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a value is written to the Item', function () {
|
||||
it('should write a string value to the underlying file', async function () {
|
||||
const item = await box.createItem('test');
|
||||
await item.write('boo bah');
|
||||
|
||||
expect(MockFs.writeFile).to.have.been.calledWith(
|
||||
path.join(DATA_DIR, DEFAULT_SUFFIX, 'test'),
|
||||
'boo bah',
|
||||
'utf8'
|
||||
);
|
||||
});
|
||||
|
||||
it('should update the underlying value', async function () {
|
||||
const item = await box.createItem('test');
|
||||
await item.write('boo bah');
|
||||
expect(item.value).to.equal('boo bah');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a Item with the same id already exists', function () {
|
||||
it('should throw an error', async function () {
|
||||
await box.createItem('test');
|
||||
await expect(box.createItem('test')).to.be.rejectedWith(
|
||||
Error,
|
||||
'Item with id "/some/dir/strongbox/test" already exists'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAll()', function () {
|
||||
let clear: sinon.SinonStub<never[], Promise<void>>;
|
||||
|
||||
beforeEach(async function () {
|
||||
const item = await box.createItem<string>('SLUG test');
|
||||
clear = sandbox.stub(item, 'clear');
|
||||
});
|
||||
|
||||
it('should call clear() on each item', async function () {
|
||||
await box.clearAll();
|
||||
expect(clear).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
describe('when there is some other error', function () {
|
||||
beforeEach(function () {
|
||||
clear.rejects(new Error('ETOOMANYGOATS'));
|
||||
});
|
||||
|
||||
it('should reject', async function () {
|
||||
await expect(box.clearAll()).to.be.rejected;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createItemWithValue()', function () {
|
||||
it('should create a Item with the given value', async function () {
|
||||
const item = await box.createItemWithValue('test', 'value');
|
||||
expect(item.value).to.equal('value');
|
||||
});
|
||||
|
||||
it('should write the value to disk', async function () {
|
||||
await box.createItemWithValue('test', 'value');
|
||||
expect(MockFs.writeFile).to.have.been.calledWith(
|
||||
path.join(DATA_DIR, DEFAULT_SUFFIX, 'test'),
|
||||
'value'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
10
packages/strongbox/tsconfig.json
Normal file
10
packages/strongbox/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@appium/tsconfig/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"strict": true,
|
||||
"types": ["node", "mocha", "chai", "chai-as-promised", "sinon-chai"]
|
||||
},
|
||||
"include": ["lib", "test"]
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"strict": false,
|
||||
"types": ["node"]
|
||||
"types": ["node"],
|
||||
"lib": ["es2020", "ES2021.WeakRef"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
},
|
||||
{
|
||||
"path": "packages/doctor"
|
||||
},
|
||||
{
|
||||
"path": "packages/strongbox"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"./packages/driver-test-support",
|
||||
"./packages/test-support",
|
||||
"./packages/plugin-test-support",
|
||||
"./packages/opencv"
|
||||
"./packages/opencv",
|
||||
"./packages/strongbox"
|
||||
],
|
||||
"name": "Appium",
|
||||
"out": "./packages/appium/docs/en/reference",
|
||||
|
||||
Reference in New Issue
Block a user