@forwardimpact/librpc 0.1.77
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/LICENSE +201 -0
- package/auth.js +269 -0
- package/base.js +126 -0
- package/client.js +220 -0
- package/index.js +60 -0
- package/interceptor.js +130 -0
- package/package.json +24 -0
- package/server.js +212 -0
- package/test/auth.test.js +94 -0
- package/test/client.test.js +138 -0
- package/test/server.test.js +123 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { test, describe, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
|
|
4
|
+
import { createAuth, HmacAuth } from "../index.js";
|
|
5
|
+
|
|
6
|
+
describe("Auth", () => {
|
|
7
|
+
describe("HmacAuth", () => {
|
|
8
|
+
test("should throw if secret is missing or too short", () => {
|
|
9
|
+
assert.throws(() => new HmacAuth(), /Secret must be a non-empty string/);
|
|
10
|
+
assert.throws(
|
|
11
|
+
() => new HmacAuth("short"),
|
|
12
|
+
/Secret must be at least 32 characters long/,
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("should generate and verify valid tokens", () => {
|
|
17
|
+
const secret = "test-secret-that-is-at-least-32-characters-long";
|
|
18
|
+
const auth = new HmacAuth(secret);
|
|
19
|
+
const serviceId = "test-service";
|
|
20
|
+
|
|
21
|
+
const token = auth.generateToken(serviceId);
|
|
22
|
+
assert.ok(token);
|
|
23
|
+
|
|
24
|
+
const result = auth.verifyToken(token);
|
|
25
|
+
assert.strictEqual(result.isValid, true);
|
|
26
|
+
assert.strictEqual(result.serviceId, serviceId);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("should reject expired tokens", async () => {
|
|
30
|
+
const secret = "test-secret-that-is-at-least-32-characters-long";
|
|
31
|
+
// 1 second lifetime
|
|
32
|
+
const auth = new HmacAuth(secret, 1);
|
|
33
|
+
const serviceId = "test-service";
|
|
34
|
+
|
|
35
|
+
const token = auth.generateToken(serviceId);
|
|
36
|
+
|
|
37
|
+
// Wait for token to expire (1.1 seconds)
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
39
|
+
|
|
40
|
+
const result = auth.verifyToken(token);
|
|
41
|
+
assert.strictEqual(result.isValid, false);
|
|
42
|
+
assert.match(result.error, /Token has expired/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("should reject invalid signatures", () => {
|
|
46
|
+
const secret = "test-secret-that-is-at-least-32-characters-long";
|
|
47
|
+
const auth = new HmacAuth(secret);
|
|
48
|
+
const serviceId = "test-service";
|
|
49
|
+
|
|
50
|
+
const token = auth.generateToken(serviceId);
|
|
51
|
+
|
|
52
|
+
// Tamper with the token (it's base64 encoded)
|
|
53
|
+
const decoded = Buffer.from(token, "base64").toString("utf8");
|
|
54
|
+
const parts = decoded.split(":");
|
|
55
|
+
// Change serviceId part
|
|
56
|
+
parts[0] = "other-service";
|
|
57
|
+
const tamperedToken = Buffer.from(parts.join(":")).toString("base64");
|
|
58
|
+
|
|
59
|
+
const result = auth.verifyToken(tamperedToken);
|
|
60
|
+
assert.strictEqual(result.isValid, false);
|
|
61
|
+
assert.match(result.error, /Invalid token signature/);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("createAuth", () => {
|
|
66
|
+
let originalEnv;
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
originalEnv = process.env.SERVICE_SECRET;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("should throw if SERVICE_SECRET is missing", () => {
|
|
73
|
+
delete process.env.SERVICE_SECRET;
|
|
74
|
+
assert.throws(
|
|
75
|
+
() => createAuth("test"),
|
|
76
|
+
/SERVICE_SECRET environment variable is required/,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("should create Interceptor with valid secret", () => {
|
|
81
|
+
process.env.SERVICE_SECRET =
|
|
82
|
+
"test-secret-that-is-at-least-32-characters-long";
|
|
83
|
+
const interceptor = createAuth("test");
|
|
84
|
+
assert.ok(interceptor);
|
|
85
|
+
|
|
86
|
+
// Cleanup
|
|
87
|
+
if (originalEnv) {
|
|
88
|
+
process.env.SERVICE_SECRET = originalEnv;
|
|
89
|
+
} else {
|
|
90
|
+
delete process.env.SERVICE_SECRET;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { test, describe, beforeEach, mock } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { PassThrough } from "stream";
|
|
4
|
+
|
|
5
|
+
import { Client } from "../index.js";
|
|
6
|
+
import {
|
|
7
|
+
createMockGrpcFn,
|
|
8
|
+
createMockObserverFn,
|
|
9
|
+
createMockAuthFn,
|
|
10
|
+
createMockLogger,
|
|
11
|
+
} from "@forwardimpact/libharness";
|
|
12
|
+
|
|
13
|
+
describe("Client", () => {
|
|
14
|
+
let mockConfig;
|
|
15
|
+
let mockGrpcFn;
|
|
16
|
+
let mockAuthFn;
|
|
17
|
+
let mockLogFn;
|
|
18
|
+
let mockObserverFn;
|
|
19
|
+
let mockClientInstance;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockConfig = {
|
|
23
|
+
name: "memory", // Use a valid service name that exists in definitions
|
|
24
|
+
host: "0.0.0.0",
|
|
25
|
+
port: 5000,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
mockClientInstance = {
|
|
29
|
+
TestMethod: mock.fn((_req, _meta, cb) => cb(null, { result: "success" })),
|
|
30
|
+
StreamMethod: mock.fn((_req, _meta) => {
|
|
31
|
+
const stream = new PassThrough({ objectMode: true });
|
|
32
|
+
process.nextTick(() => {
|
|
33
|
+
stream.emit("metadata", {});
|
|
34
|
+
stream.write({ result: "chunk1" });
|
|
35
|
+
stream.write({ result: "chunk2" });
|
|
36
|
+
stream.end();
|
|
37
|
+
});
|
|
38
|
+
return stream;
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
mockGrpcFn = createMockGrpcFn({
|
|
43
|
+
grpc: {
|
|
44
|
+
Metadata: class {},
|
|
45
|
+
makeGenericClientConstructor: mock.fn(() => {
|
|
46
|
+
return function () {
|
|
47
|
+
return mockClientInstance;
|
|
48
|
+
};
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
mockAuthFn = createMockAuthFn();
|
|
54
|
+
|
|
55
|
+
mockLogFn = createMockLogger();
|
|
56
|
+
|
|
57
|
+
mockObserverFn = createMockObserverFn();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("should require config parameter", () => {
|
|
61
|
+
assert.throws(
|
|
62
|
+
() =>
|
|
63
|
+
new Client(
|
|
64
|
+
null,
|
|
65
|
+
mockLogFn,
|
|
66
|
+
null,
|
|
67
|
+
mockObserverFn,
|
|
68
|
+
mockGrpcFn,
|
|
69
|
+
mockAuthFn,
|
|
70
|
+
),
|
|
71
|
+
/config is required/,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("should accept valid parameters", () => {
|
|
76
|
+
const client = new Client(
|
|
77
|
+
mockConfig,
|
|
78
|
+
mockLogFn,
|
|
79
|
+
null,
|
|
80
|
+
mockObserverFn,
|
|
81
|
+
mockGrpcFn,
|
|
82
|
+
mockAuthFn,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
assert.ok(client);
|
|
86
|
+
assert.strictEqual(client.config, mockConfig);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("callUnary should execute unary call", async () => {
|
|
90
|
+
const client = new Client(
|
|
91
|
+
mockConfig,
|
|
92
|
+
mockLogFn,
|
|
93
|
+
null,
|
|
94
|
+
mockObserverFn,
|
|
95
|
+
mockGrpcFn,
|
|
96
|
+
mockAuthFn,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const response = await client.callUnary("TestMethod", { some: "data" });
|
|
100
|
+
assert.deepStrictEqual(response, { result: "success" });
|
|
101
|
+
assert.strictEqual(mockClientInstance.TestMethod.mock.callCount(), 1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("callStream should execute streaming call", async () => {
|
|
105
|
+
const client = new Client(
|
|
106
|
+
mockConfig,
|
|
107
|
+
mockLogFn,
|
|
108
|
+
null,
|
|
109
|
+
mockObserverFn,
|
|
110
|
+
mockGrpcFn,
|
|
111
|
+
mockAuthFn,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const stream = client.callStream("StreamMethod", { some: "data" });
|
|
115
|
+
|
|
116
|
+
const chunks = [];
|
|
117
|
+
for await (const chunk of stream) {
|
|
118
|
+
chunks.push(chunk);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
assert.strictEqual(chunks.length, 2);
|
|
122
|
+
assert.deepStrictEqual(chunks[0], { result: "chunk1" });
|
|
123
|
+
assert.deepStrictEqual(chunks[1], { result: "chunk2" });
|
|
124
|
+
assert.strictEqual(mockClientInstance.StreamMethod.mock.callCount(), 1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("should accept tracer parameter", () => {
|
|
128
|
+
const client = new Client(
|
|
129
|
+
mockConfig,
|
|
130
|
+
mockLogFn,
|
|
131
|
+
{}, // tracer
|
|
132
|
+
mockObserverFn,
|
|
133
|
+
mockGrpcFn,
|
|
134
|
+
mockAuthFn,
|
|
135
|
+
);
|
|
136
|
+
assert.ok(client);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { test, describe, beforeEach, mock } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
|
|
4
|
+
import { Server } from "../index.js";
|
|
5
|
+
import {
|
|
6
|
+
createMockConfig,
|
|
7
|
+
createMockGrpcFn,
|
|
8
|
+
createMockObserverFn,
|
|
9
|
+
createMockAuthFn,
|
|
10
|
+
createMockLogger,
|
|
11
|
+
createMockTracer,
|
|
12
|
+
} from "@forwardimpact/libharness";
|
|
13
|
+
|
|
14
|
+
describe("Server", () => {
|
|
15
|
+
let mockService;
|
|
16
|
+
let mockConfig;
|
|
17
|
+
let mockGrpcFn;
|
|
18
|
+
let mockAuthFn;
|
|
19
|
+
let mockLogFn;
|
|
20
|
+
let mockObserverFn;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mockService = {
|
|
24
|
+
getHandlers: () => ({
|
|
25
|
+
TestMethod: async (_call) => ({ result: "test" }),
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
mockConfig = createMockConfig("memory", { host: "0.0.0.0", port: 5000 });
|
|
30
|
+
mockGrpcFn = createMockGrpcFn();
|
|
31
|
+
mockAuthFn = createMockAuthFn();
|
|
32
|
+
mockLogFn = createMockLogger();
|
|
33
|
+
mockObserverFn = createMockObserverFn(mockLogFn);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("should require service parameter", () => {
|
|
37
|
+
assert.throws(
|
|
38
|
+
() =>
|
|
39
|
+
new Server(
|
|
40
|
+
null,
|
|
41
|
+
mockConfig,
|
|
42
|
+
mockLogFn,
|
|
43
|
+
null,
|
|
44
|
+
mockObserverFn,
|
|
45
|
+
mockGrpcFn,
|
|
46
|
+
mockAuthFn,
|
|
47
|
+
),
|
|
48
|
+
/service is required/,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("should require config parameter", () => {
|
|
53
|
+
assert.throws(
|
|
54
|
+
() =>
|
|
55
|
+
new Server(
|
|
56
|
+
mockService,
|
|
57
|
+
null,
|
|
58
|
+
mockLogFn,
|
|
59
|
+
null,
|
|
60
|
+
mockObserverFn,
|
|
61
|
+
mockGrpcFn,
|
|
62
|
+
mockAuthFn,
|
|
63
|
+
),
|
|
64
|
+
/config is required/,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("should accept valid parameters", () => {
|
|
69
|
+
const server = new Server(
|
|
70
|
+
mockService,
|
|
71
|
+
mockConfig,
|
|
72
|
+
mockLogFn,
|
|
73
|
+
null,
|
|
74
|
+
mockObserverFn,
|
|
75
|
+
mockGrpcFn,
|
|
76
|
+
mockAuthFn,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
assert.ok(server);
|
|
80
|
+
assert.strictEqual(server.config, mockConfig);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("should call service methods during setup", async () => {
|
|
84
|
+
const getHandlersSpy = mock.fn(() => ({
|
|
85
|
+
TestMethod: async () => ({ result: "test" }),
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
const spiedService = {
|
|
89
|
+
getHandlers: getHandlersSpy,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const server = new Server(
|
|
93
|
+
spiedService,
|
|
94
|
+
mockConfig,
|
|
95
|
+
mockLogFn,
|
|
96
|
+
null,
|
|
97
|
+
mockObserverFn,
|
|
98
|
+
mockGrpcFn,
|
|
99
|
+
mockAuthFn,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Start the server to trigger setup
|
|
103
|
+
await server.start();
|
|
104
|
+
|
|
105
|
+
assert.strictEqual(getHandlersSpy.mock.callCount(), 1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("should accept tracer parameter", () => {
|
|
109
|
+
const mockTracer = createMockTracer();
|
|
110
|
+
|
|
111
|
+
const server = new Server(
|
|
112
|
+
mockService,
|
|
113
|
+
mockConfig,
|
|
114
|
+
mockLogFn,
|
|
115
|
+
mockTracer,
|
|
116
|
+
mockObserverFn,
|
|
117
|
+
mockGrpcFn,
|
|
118
|
+
mockAuthFn,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
assert.ok(server);
|
|
122
|
+
});
|
|
123
|
+
});
|