@sockethub/data-layer 1.0.0-alpha.3 → 1.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API.md ADDED
@@ -0,0 +1,23 @@
1
+ # API Documentation
2
+
3
+ This package provides the data layer for Sockethub, including job queue management and secure credential storage.
4
+
5
+ ## Main Classes
6
+
7
+ ### JobQueue
8
+ Redis-backed job queue for managing ActivityStreams message processing. Creates isolated queues per platform instance and session, providing reliable message delivery and processing coordination between Sockethub server and platform workers.
9
+
10
+ ### JobWorker
11
+ Worker for processing jobs from a Redis queue within platform child processes. Connects to the same queue as its corresponding JobQueue instance and processes jobs using a platform-specific handler function.
12
+
13
+ ### CredentialsStore
14
+ Secure, encrypted storage for user credentials with session-based isolation. Provides automatic encryption/decryption of credential objects stored in Redis, ensuring that sensitive authentication data is never stored in plaintext.
15
+
16
+ ## Complete API Reference
17
+
18
+ For detailed API documentation including method signatures, parameters, and examples, see the [generated TypeDoc documentation](./docs/index.html).
19
+
20
+ ## Related Documentation
21
+
22
+ - [Queue Architecture](./QUEUE.md) - Conceptual overview of the queueing system
23
+ - [Main Sockethub Documentation](https://github.com/sockethub/sockethub)
package/QUEUE.md ADDED
@@ -0,0 +1,130 @@
1
+ # Sockethub Queue System
2
+
3
+ The queue system is Sockethub's core infrastructure that enables reliable, scalable
4
+ communication between web clients and protocol platforms.
5
+
6
+ ## Why a Queue System?
7
+
8
+ Web applications need to communicate with network protocols (IRC, XMPP, RSS) that:
9
+
10
+ - Have slow or unreliable connections
11
+ - Require persistent state that browsers can't maintain
12
+ - Need to operate across multiple concurrent user sessions
13
+
14
+ The queue system solves this by:
15
+
16
+ - **Decoupling** web clients from protocol complexity
17
+ - **Ensuring reliability** through message persistence
18
+ - **Enabling concurrency** via process isolation
19
+ - **Maintaining security** through encrypted message handling
20
+
21
+ ## Architecture Overview
22
+
23
+ ```
24
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
25
+ │ Web Clients │ │ Sockethub │ │ Protocol │
26
+ │ │ │ Queue System │ │ Platforms │
27
+ │ • Browser Apps │◄──►│ │◄──►│ │
28
+ │ • Mobile Apps │ │ • Job Queues │ │ • IRC Client │
29
+ │ • Desktop Apps │ │ • Message │ │ • XMPP Client │
30
+ │ • APIs │ │ Routing │ │ • RSS Parser │
31
+ │ │ │ • Session Mgmt │ │ • Metadata │
32
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
33
+ ```
34
+
35
+ ## Message Flow
36
+
37
+ ### 1. Web Request → Queue
38
+
39
+ ```
40
+ Web App → Socket.IO → Sockethub Server → Redis Queue
41
+ ```
42
+
43
+ - Client sends ActivityStreams message
44
+ - Server validates and queues the request
45
+ - Client gets immediate acknowledgment
46
+
47
+ ### 2. Queue → Platform Processing
48
+
49
+ ```
50
+ Redis Queue → Platform Worker → External Protocol → Response
51
+ ```
52
+
53
+ - Platform worker picks up job
54
+ - Processes protocol-specific logic in isolation
55
+ - Connects to external services (IRC, XMPP, etc.)
56
+
57
+ ### 3. Response → Client
58
+
59
+ ```
60
+ Platform Result → Queue Event → Sockethub Server → Socket.IO → Web App
61
+ ```
62
+
63
+ - Results flow back through the queue
64
+ - Real-time updates delivered via Socket.IO
65
+ - Clients receive responses and ongoing events
66
+
67
+ ## Key Design Principles
68
+
69
+ **Process Isolation**: Each protocol runs in its own child process
70
+
71
+ - Crashed platforms don't affect others
72
+ - Memory leaks and resource issues are contained
73
+ - Different platforms can use different dependencies
74
+
75
+ **Session-Based Queues**: Each client gets dedicated queue infrastructure
76
+
77
+ - Complete privacy between users
78
+ - Natural load distribution
79
+ - Session-specific configurations possible
80
+
81
+ **Message Durability**: All messages persist until processed
82
+
83
+ - Server restarts don't lose messages
84
+ - Failed operations can be retried
85
+ - Audit trail for debugging
86
+
87
+ ## Real-World Example
88
+
89
+ A chat application connecting to both IRC and XMPP:
90
+
91
+ ```
92
+ User message → Queue → [IRC Platform] + [XMPP Platform]
93
+ ↓ ↓
94
+ IRC Network XMPP Network
95
+ ```
96
+
97
+ **Benefits:**
98
+
99
+ - Both protocols process concurrently
100
+ - Slow IRC doesn't block XMPP messages
101
+ - Platform crashes only affect that protocol
102
+ - Independent delivery confirmations
103
+
104
+ ## Operational Characteristics
105
+
106
+ **Reliability**: Messages are never lost, even during:
107
+
108
+ - Server restarts
109
+ - Platform process crashes
110
+ - Temporary network failures
111
+ - Redis downtime
112
+
113
+ **Scalability**: Natural horizontal scaling:
114
+
115
+ - Multiple server instances share Redis
116
+ - Per-session queue isolation
117
+ - No coordination between servers required
118
+
119
+ **Performance**: Asynchronous design provides:
120
+
121
+ - Immediate web request responses
122
+ - Concurrent protocol processing
123
+ - Efficient resource utilization
124
+
125
+ ## Related Documentation
126
+
127
+ - [Data Layer Package README](README.md) - Package overview and usage
128
+ - [Main Sockethub README](../../README.md) - Overall project documentation
129
+ - [BullMQ Documentation](https://bullmq.io/) - Underlying queue library
130
+ - [ActivityStreams Specification](https://www.w3.org/TR/activitystreams-core/) - Message format standard
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @sockethub/data-layer
2
+
3
+ Redis-based data storage and job queue infrastructure for Sockethub, providing reliable
4
+ message processing and secure credential storage.
5
+
6
+ ## About
7
+
8
+ The data layer package provides the core infrastructure components that enable
9
+ Sockethub's distributed architecture:
10
+
11
+ - **Job Queue System**: Redis-backed message queuing using BullMQ for reliable ActivityStreams processing
12
+ - **Credentials Store**: Encrypted storage for user credentials and authentication data
13
+ - **Session Management**: Per-client data isolation and security
14
+ - **Redis Integration**: Connection management and health checking
15
+
16
+ ## Core Components
17
+
18
+ ### JobQueue & JobWorker
19
+
20
+ Handles asynchronous message processing between Sockethub server and platform instances.
21
+ The JobQueue manages job creation and queuing on the server side, while JobWorker
22
+ processes jobs within platform child processes.
23
+
24
+ ### CredentialsStore
25
+
26
+ Provides secure, encrypted storage for user credentials with automatic
27
+ encryption/decryption and session-based isolation.
28
+
29
+ ## Key Features
30
+
31
+ ### Security
32
+
33
+ - **End-to-end encryption** of all stored data using AES-256
34
+ - **Session-based isolation** prevents credential cross-contamination
35
+ - **Secure key management** with per-session secrets
36
+ - **No plaintext storage** in Redis or logs
37
+
38
+ ### Reliability
39
+
40
+ - **Message persistence** ensures no data loss during processing
41
+ - **Automatic retries** for failed operations
42
+ - **Connection pooling** with Redis for efficiency
43
+ - **Graceful error handling** and recovery
44
+
45
+ ### Scalability
46
+
47
+ - **Horizontal scaling** across multiple server instances
48
+ - **Per-session queues** for natural load distribution
49
+ - **Process isolation** for platform-specific processing
50
+ - **Redis clustering** support for high availability
51
+
52
+ ## Configuration
53
+
54
+ **Redis Configuration**: Requires a Redis URL in the format `redis://host:port` or `redis://user:pass@host:port`
55
+
56
+ **Dependencies:**
57
+
58
+ - Redis server (6.0+)
59
+ - `@sockethub/crypto` for encryption
60
+ - `@sockethub/schemas` for type definitions
61
+
62
+ ## API Documentation
63
+
64
+ For detailed API documentation including method signatures, parameters, and examples,
65
+ generate the API docs from JSDoc comments:
66
+
67
+ ```bash
68
+ bun run doc
69
+ ```
70
+
71
+ ## Architecture Integration
72
+
73
+ The data layer sits between Sockethub's main server and platform instances:
74
+
75
+ ```
76
+ [Sockethub Server] ←→ [Data Layer] ←→ [Platform Instances]
77
+ ↑ ↑ ↑
78
+ Socket.IO Redis Storage Protocol Logic
79
+ Connections Job Queues (IRC, XMPP, etc.)
80
+ ```
81
+
82
+ **Server Integration**: Creates job queues and credential stores per client session
83
+
84
+ **Platform Integration**: Workers process jobs and access stored credentials securely
85
+
86
+ **Redis Storage**: Centralized, persistent storage for all data layer operations
87
+
88
+ ## Error Handling
89
+
90
+ The data layer provides comprehensive error handling:
91
+
92
+ - **Connection failures**: Automatic retry with exponential backoff
93
+ - **Encryption errors**: Clear error messages for key management issues
94
+ - **Job failures**: Failed jobs are logged and can be retried
95
+ - **Resource cleanup**: Proper shutdown procedures prevent memory leaks
96
+
97
+ ## Performance Considerations
98
+
99
+ **Memory Management:**
100
+
101
+ - Completed jobs are automatically cleaned up
102
+ - Connection pooling minimizes resource usage
103
+ - Per-session isolation prevents memory leaks
104
+
105
+ **Network Efficiency:**
106
+
107
+ - Redis pipelining for batch operations
108
+ - Compressed job payloads for large messages
109
+ - Keep-alive connections reduce overhead
110
+
111
+ ## Related Documentation
112
+
113
+ - [Main Sockethub README](../../README.md) - Overall project documentation
114
+ - [Queue System Architecture](QUEUE.md) - Overview of the Queue system
package/package.json CHANGED
@@ -1,21 +1,20 @@
1
1
  {
2
2
  "name": "@sockethub/data-layer",
3
3
  "description": "Storing and RPC of data for Sockethub",
4
- "version": "1.0.0-alpha.3",
4
+ "version": "1.0.0-alpha.5",
5
+ "type": "module",
5
6
  "private": false,
6
7
  "author": "Nick Jennings <nick@silverbucket.net>",
7
8
  "license": "MIT",
8
- "main": "dist/index.js",
9
- "types": "dist/index.d.ts",
10
- "files": [
11
- "dist/",
12
- "src/"
13
- ],
9
+ "main": "src/index.ts",
10
+ "engines": {
11
+ "bun": ">=1.2"
12
+ },
14
13
  "keywords": [
15
14
  "sockethub",
16
15
  "messaging",
17
16
  "redis",
18
- "data lyer",
17
+ "data layer",
19
18
  "rpc",
20
19
  "data store"
21
20
  ],
@@ -25,44 +24,24 @@
25
24
  "directory": "packages/data-layer"
26
25
  },
27
26
  "homepage": "https://github.com/sockethub/sockethub/tree/master/packages/data-layer",
28
- "dependencies": {
29
- "@sockethub/crypto": "^1.0.0-alpha.3",
30
- "@sockethub/schemas": "^3.0.0-alpha.3",
31
- "bull": "^4.0.0",
32
- "debug": "^4.3.1",
33
- "secure-store-redis": "^1.4.7"
34
- },
35
27
  "scripts": {
36
- "clean": "npx rimraf dist coverage",
37
- "clean:deps": "npx rimraf node_modules",
38
- "compliance": "yarn run lint && yarn run test && yarn run coverage",
39
- "test": "c8 -x \"src/**/*.test.*\" mocha -r ts-node/register src/*.test.ts",
40
- "coverage": "c8 check-coverage --statements 85 --branches 90 --functions 100 --lines 85",
41
- "lint": "eslint \"**/*.ts\"",
42
- "lint:fix": "eslint --fix \"**/*.ts\"",
43
- "build": "tsc"
28
+ "doc": "typedoc --options typedoc.json"
44
29
  },
45
- "engines": {
46
- "node": ">= 14"
30
+ "dependencies": {
31
+ "@sockethub/crypto": "1.0.0-alpha.5",
32
+ "@sockethub/schemas": "3.0.0-alpha.5",
33
+ "bullmq": "5.66.4",
34
+ "debug": "4.3.4",
35
+ "ioredis": "5.3.2",
36
+ "secure-store-redis": "3.0.7"
47
37
  },
48
38
  "devDependencies": {
49
- "@types/chai": "4.3.3",
50
- "@types/debug": "4.1.7",
51
- "@types/eslint": "8.4.6",
52
- "@types/mocha": "9.1.1",
53
- "@types/node": "17.0.13",
54
- "@types/proxyquire": "1.3.28",
55
- "@types/sinon": "10.0.13",
56
- "@typescript-eslint/parser": "5.37.0",
57
- "c8": "7.12.0",
58
- "chai": "4.3.6",
59
- "eslint": "8.23.1",
60
- "eslint-cli": "1.1.1",
61
- "mocha": "10.0.0",
62
- "proxyquire": "2.1.3",
63
- "sinon": "14.0.0",
64
- "ts-node": "10.9.1",
65
- "typescript": "4.8.3"
39
+ "@types/bun": "latest",
40
+ "@types/debug": "4.1.12",
41
+ "@types/sinon": "17.0.2",
42
+ "sinon": "17.0.1",
43
+ "typedoc": "^0.28.14",
44
+ "typedoc-plugin-markdown": "^4.9.0"
66
45
  },
67
- "gitHead": "1fa39894aafb49dfea7a7425bdaf196325e81e5d"
46
+ "gitHead": "341ea9eeca6afd1442fe6e01457bc21d112b91a4"
68
47
  }
@@ -1,138 +1,155 @@
1
- import proxyquire from 'proxyquire';
2
- import { expect } from 'chai';
3
- import * as sinon from 'sinon';
1
+ import { beforeEach, describe, expect, it } from "bun:test";
2
+ import * as sinon from "sinon";
4
3
 
5
- proxyquire.noPreserveCache();
6
- proxyquire.noCallThru();
4
+ import { CredentialsStore } from "./credentials-store";
7
5
 
8
- describe('CredentialsStore', () => {
9
- let credentialsStore, MockSecureStore, MockStoreGet, MockStoreSave, MockObjectHash;
10
- beforeEach(() => {
11
- MockStoreGet = sinon.stub().callsArgWith(1, undefined, 'credential foo');
12
- MockStoreSave = sinon.stub().callsArgWith(2, undefined);
13
- MockObjectHash = sinon.stub();
14
- MockSecureStore = sinon.stub().returns({
15
- get: MockStoreGet,
16
- save: MockStoreSave,
6
+ describe("CredentialsStore", () => {
7
+ let credentialsStore,
8
+ MockSecureStore,
9
+ MockStoreGet,
10
+ MockStoreSave,
11
+ MockObjectHash;
12
+ beforeEach(() => {
13
+ MockStoreGet = sinon.stub().returns("credential foo");
14
+ MockStoreSave = sinon.stub();
15
+ MockObjectHash = sinon.stub();
16
+ MockSecureStore = sinon.stub().returns({
17
+ get: MockStoreGet,
18
+ save: MockStoreSave,
19
+ });
20
+ class TestCredentialsStore extends CredentialsStore {
21
+ initCrypto() {
22
+ this.objectHash = MockObjectHash;
23
+ }
24
+ initSecureStore(secret, redisConfig) {
25
+ this.store = MockSecureStore({
26
+ namespace: "foo",
27
+ secret: secret,
28
+ redis: redisConfig,
29
+ });
30
+ }
31
+ }
32
+ credentialsStore = new TestCredentialsStore(
33
+ "a parent id",
34
+ "a session id",
35
+ "a secret must be 32 chars and th",
36
+ { url: "redis config" },
37
+ );
17
38
  });
18
- const StoreMod = proxyquire('./credentials-store', {
19
- 'secure-store-redis': MockSecureStore,
20
- '@sockethub/crypto': {
21
- objectHash: MockObjectHash
22
- }
23
- });
24
- const CredentialsStore = StoreMod.default;
25
- credentialsStore = new CredentialsStore('a parent id', 'a session id', 'a secret', 'redis config');
26
- });
27
39
 
28
- it('returns a valid CredentialsStore object', () => {
29
- sinon.assert.calledOnce(MockSecureStore);
30
- sinon.assert.calledWith(MockSecureStore, {
31
- namespace: 'sockethub:data-layer:credentials-store:a parent id:a session id',
32
- secret: 'a secret',
33
- redis: 'redis config'
40
+ it("returns a valid CredentialsStore object", () => {
41
+ sinon.assert.calledOnce(MockSecureStore);
42
+ sinon.assert.calledWith(MockSecureStore, {
43
+ namespace: "foo",
44
+ secret: "a secret must be 32 chars and th",
45
+ redis: { url: "redis config" },
46
+ });
47
+ expect(typeof credentialsStore).toEqual("object");
48
+ expect(credentialsStore.uid).toEqual(
49
+ `sockethub:data-layer:credentials-store:a parent id:a session id`,
50
+ );
51
+ expect(typeof credentialsStore.get).toEqual("function");
52
+ expect(typeof credentialsStore.save).toEqual("function");
34
53
  });
35
- expect(typeof credentialsStore).to.equal('object');
36
- expect(credentialsStore.uid).to.equal(`sockethub:data-layer:credentials-store:a parent id:a session id`);
37
- expect(typeof credentialsStore.get).to.equal('function');
38
- expect(typeof credentialsStore.save).to.equal('function');
39
- });
40
54
 
41
- describe('get', () => {
42
- it('handles correct params', async () => {
43
- let res = await credentialsStore.get('an actor');
44
- sinon.assert.calledOnce(MockStoreGet);
45
- sinon.assert.calledWith(MockStoreGet, 'an actor');
46
- sinon.assert.notCalled(MockObjectHash);
47
- sinon.assert.notCalled(MockStoreSave);
48
- expect(res).to.equal('credential foo');
49
- });
55
+ describe("get", () => {
56
+ it("handles correct params", async () => {
57
+ const res = await credentialsStore.get("an actor");
58
+ sinon.assert.calledOnce(MockStoreGet);
59
+ sinon.assert.calledWith(MockStoreGet, "an actor");
60
+ sinon.assert.notCalled(MockObjectHash);
61
+ sinon.assert.notCalled(MockStoreSave);
62
+ expect(res).toEqual("credential foo");
63
+ });
50
64
 
51
- it('handles no credentials found', async () => {
52
- MockStoreGet.callsArgWith(1, undefined, undefined);
53
- let res = await credentialsStore.get('an non-existent actor');
54
- sinon.assert.calledOnce(MockStoreGet);
55
- sinon.assert.calledWith(MockStoreGet, 'an non-existent actor');
56
- sinon.assert.notCalled(MockObjectHash);
57
- sinon.assert.notCalled(MockStoreSave);
58
- expect(res).to.be.undefined;
59
- });
65
+ it("handles no credentials found", async () => {
66
+ MockStoreGet.returns(undefined);
67
+ expect(async () => {
68
+ await credentialsStore.get("a non-existent actor");
69
+ }).toThrow("credentials not found for a non-existent actor");
70
+ sinon.assert.calledOnce(MockStoreGet);
71
+ sinon.assert.calledWith(MockStoreGet, "a non-existent actor");
72
+ sinon.assert.notCalled(MockObjectHash);
73
+ sinon.assert.notCalled(MockStoreSave);
74
+ });
60
75
 
61
- it('handles an unexpected error', async () => {
62
- MockStoreGet.callsArgWith(1, 'sumting bad happen', undefined);
63
- let res;
64
- try {
65
- res = await credentialsStore.get('a problem actor');
66
- throw new Error('should not reach this spot');
67
- } catch (err) {
68
- expect(err).to.equal('sumting bad happen');
69
- }
70
- sinon.assert.calledOnce(MockStoreGet);
71
- sinon.assert.calledWith(MockStoreGet, 'a problem actor');
72
- sinon.assert.notCalled(MockObjectHash);
73
- sinon.assert.notCalled(MockStoreSave);
74
- expect(res).to.be.undefined;
75
- });
76
+ it("handles an unexpected error", async () => {
77
+ MockStoreGet.returns(undefined);
78
+ try {
79
+ await credentialsStore.get("a problem actor");
80
+ expect(false).toEqual(true);
81
+ } catch (err) {
82
+ expect(err.toString()).toEqual(
83
+ "Error: credentials not found for a problem actor",
84
+ );
85
+ }
86
+ sinon.assert.calledOnce(MockStoreGet);
87
+ sinon.assert.calledWith(MockStoreGet, "a problem actor");
88
+ sinon.assert.notCalled(MockObjectHash);
89
+ sinon.assert.notCalled(MockStoreSave);
90
+ });
76
91
 
77
- it('validates credentialsHash when provided', async () => {
78
- MockObjectHash.returns('a credentialHash string');
79
- MockStoreGet.callsArgWith(1, undefined, {
80
- object: 'a credential'
81
- });
82
- let res = await credentialsStore.get('an actor', 'a credentialHash string');
83
- sinon.assert.calledOnce(MockStoreGet);
84
- sinon.assert.calledWith(MockStoreGet, 'an actor');
85
- sinon.assert.calledOnce(MockObjectHash);
86
- sinon.assert.calledWith(MockObjectHash, 'a credential');
87
- sinon.assert.notCalled(MockStoreSave);
88
- expect(res).to.eql({object: 'a credential'});
89
- });
92
+ it("validates credentialsHash when provided", async () => {
93
+ MockObjectHash.returns("a credentialsHash string");
94
+ MockStoreGet.returns({
95
+ object: "a credential",
96
+ });
97
+ const res = await credentialsStore.get(
98
+ "an actor",
99
+ "a credentialsHash string",
100
+ );
101
+ sinon.assert.calledOnce(MockStoreGet);
102
+ sinon.assert.calledWith(MockStoreGet, "an actor");
103
+ sinon.assert.calledOnce(MockObjectHash);
104
+ sinon.assert.calledWith(MockObjectHash, "a credential");
105
+ sinon.assert.notCalled(MockStoreSave);
106
+ expect(res).toEqual({ object: "a credential" });
107
+ });
90
108
 
91
- it('invalidates credentialsHash when provided', async () => {
92
- MockObjectHash.returns('the original credentialHash string');
93
- MockStoreGet.callsArgWith(1, undefined, {
94
- object: 'a credential'
95
- });
96
- let res;
97
- try {
98
- res = await credentialsStore.get('an actor', 'a different credentialHash string');
99
- throw new Error('should not reach this spot');
100
- } catch (err) {
101
- expect(err).to.equal('provided credentials do not match existing platform instance for actor an actor');
102
- }
103
- sinon.assert.calledOnce(MockStoreGet);
104
- sinon.assert.calledWith(MockStoreGet, 'an actor');
105
- sinon.assert.calledOnce(MockObjectHash);
106
- sinon.assert.calledWith(MockObjectHash, 'a credential');
107
- sinon.assert.notCalled(MockStoreSave);
108
- expect(res).to.be.undefined;
109
+ it("invalidates credentialsHash when provided", async () => {
110
+ MockObjectHash.returns("the original credentialsHash string");
111
+ MockStoreGet.returns({
112
+ object: "a credential",
113
+ });
114
+ try {
115
+ expect(
116
+ await credentialsStore.get(
117
+ "an actor",
118
+ "a different credentialsHash string",
119
+ ),
120
+ ).toBeUndefined();
121
+ expect(false).toEqual(true);
122
+ } catch (err) {
123
+ expect(err.toString()).toEqual(
124
+ "Error: invalid credentials for an actor",
125
+ );
126
+ }
127
+ sinon.assert.calledOnce(MockStoreGet);
128
+ sinon.assert.calledWith(MockStoreGet, "an actor");
129
+ sinon.assert.calledOnce(MockObjectHash);
130
+ sinon.assert.calledWith(MockObjectHash, "a credential");
131
+ sinon.assert.notCalled(MockStoreSave);
132
+ });
109
133
  });
110
- });
111
134
 
112
- describe('save', () => {
113
- it('handles success', (done) => {
114
- const creds = {foo:'bar'};
115
- credentialsStore.save('an actor', creds, (err) => {
116
- sinon.assert.calledOnce(MockStoreSave);
117
- sinon.assert.calledWith(MockStoreSave, 'an actor', creds);
118
- sinon.assert.notCalled(MockObjectHash);
119
- sinon.assert.notCalled(MockStoreGet);
120
- expect(err).to.be.undefined;
121
- done();
122
- });
123
- });
135
+ describe("save", () => {
136
+ it("handles success", async () => {
137
+ const creds = { foo: "bar" };
138
+ await credentialsStore.save("an actor", creds);
139
+ sinon.assert.calledOnce(MockStoreSave);
140
+ sinon.assert.calledWith(MockStoreSave, "an actor", creds);
141
+ sinon.assert.notCalled(MockObjectHash);
142
+ sinon.assert.notCalled(MockStoreGet);
143
+ });
124
144
 
125
- it('handles failure', (done) => {
126
- const creds = {foo:'bar'};
127
- MockStoreSave.callsArgWith(2, 'an error');
128
- credentialsStore.save('an actor', creds, (err) => {
129
- sinon.assert.calledOnce(MockStoreSave);
130
- sinon.assert.calledWith(MockStoreSave, 'an actor', creds);
131
- sinon.assert.notCalled(MockObjectHash);
132
- sinon.assert.notCalled(MockStoreGet);
133
- expect(err).to.equal('an error');
134
- done();
135
- });
145
+ it("handles failure", async () => {
146
+ const creds = { foo: "bar" };
147
+ MockStoreSave.returns(undefined);
148
+ await credentialsStore.save("an actor", creds);
149
+ sinon.assert.calledOnce(MockStoreSave);
150
+ sinon.assert.calledWith(MockStoreSave, "an actor", creds);
151
+ sinon.assert.notCalled(MockObjectHash);
152
+ sinon.assert.notCalled(MockStoreGet);
153
+ });
136
154
  });
137
- });
138
155
  });