@quenty/influxdbclient 7.38.0 → 7.39.0
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/CHANGELOG.md +6 -0
- package/package.json +2 -2
- package/src/Server/InfluxDBClient.lua +45 -3
- package/src/Server/InfluxDBClient.spec.lua +227 -0
- package/src/Server/InfluxDBService.lua +147 -0
- package/src/Server/InfluxDBService.spec.lua +151 -0
- package/src/Server/Mocks/InfluxDBRequestHandlerMock.lua +132 -0
- package/src/Server/Mocks/InfluxDBRequestHandlerMock.spec.lua +107 -0
- package/src/Server/Write/InfluxDBWriteAPI.lua +21 -2
- package/src/Server/Write/InfluxDBWriteAPI.spec.lua +322 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [7.39.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/influxdbclient@7.38.0...@quenty/influxdbclient@7.39.0) (2026-07-22)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- Add tests to InfluxDBClient package ([c2b138f](https://github.com/Quenty/NevermoreEngine/commit/c2b138f0c0ed050dd6cb3541f644b766b5db6b10))
|
|
11
|
+
|
|
6
12
|
# [7.38.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/influxdbclient@7.37.0...@quenty/influxdbclient@7.38.0) (2026-07-21)
|
|
7
13
|
|
|
8
14
|
**Note:** Version bump only for package @quenty/influxdbclient
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/influxdbclient",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.39.0",
|
|
4
4
|
"description": "Provides a Roblox Lua InfluxDB client",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "93f15b200c0f465a0b5304fcf05f5cfe7fdcbd1d"
|
|
51
51
|
}
|
|
@@ -10,6 +10,7 @@ local require = require(script.Parent.loader).load(script)
|
|
|
10
10
|
|
|
11
11
|
local BaseObject = require("BaseObject")
|
|
12
12
|
local InfluxDBClientConfigUtils = require("InfluxDBClientConfigUtils")
|
|
13
|
+
local InfluxDBRequestHandlerMock = require("InfluxDBRequestHandlerMock")
|
|
13
14
|
local InfluxDBWriteAPI = require("InfluxDBWriteAPI")
|
|
14
15
|
local Maid = require("Maid")
|
|
15
16
|
local Promise = require("Promise")
|
|
@@ -26,6 +27,7 @@ export type InfluxDBClient =
|
|
|
26
27
|
_clientConfig: ValueObject.ValueObject<InfluxDBClientConfigUtils.InfluxDBClientConfig>,
|
|
27
28
|
_writeApis: { [string]: { [string]: InfluxDBWriteAPI.InfluxDBWriteAPI } },
|
|
28
29
|
_flushAllPromises: Promise.Promise<()>,
|
|
30
|
+
_requestHandler: InfluxDBWriteAPI.InfluxDBRequestHandler?,
|
|
29
31
|
},
|
|
30
32
|
{} :: typeof({ __index = InfluxDBClient })
|
|
31
33
|
))
|
|
@@ -35,11 +37,20 @@ export type InfluxDBClient =
|
|
|
35
37
|
Creates a new InfluxDB client
|
|
36
38
|
|
|
37
39
|
@param clientConfig InfluxDBClientConfig?
|
|
40
|
+
@param requestHandler InfluxDBRequestHandler? -- Defaults to [HttpPromise.request]. Inject a mock to avoid real HTTP.
|
|
38
41
|
@return InfluxDBClient
|
|
39
42
|
]=]
|
|
40
|
-
function InfluxDBClient.new(
|
|
43
|
+
function InfluxDBClient.new(
|
|
44
|
+
clientConfig: InfluxDBClientConfigUtils.InfluxDBClientConfig?,
|
|
45
|
+
requestHandler: InfluxDBWriteAPI.InfluxDBRequestHandler?
|
|
46
|
+
): InfluxDBClient
|
|
41
47
|
local self: InfluxDBClient = setmetatable(BaseObject.new() :: any, InfluxDBClient)
|
|
42
48
|
|
|
49
|
+
assert(type(requestHandler) == "function" or requestHandler == nil, "Bad requestHandler")
|
|
50
|
+
|
|
51
|
+
-- Threaded down into each write API created by GetWriteAPI so all of them share the same seam.
|
|
52
|
+
self._requestHandler = requestHandler
|
|
53
|
+
|
|
43
54
|
self._clientConfig = self._maid:Add(ValueObject.new(nil))
|
|
44
55
|
|
|
45
56
|
if clientConfig then
|
|
@@ -51,6 +62,33 @@ function InfluxDBClient.new(clientConfig: InfluxDBClientConfigUtils.InfluxDBClie
|
|
|
51
62
|
return self
|
|
52
63
|
end
|
|
53
64
|
|
|
65
|
+
--[=[
|
|
66
|
+
Creates a new InfluxDB client wired to an [InfluxDBRequestHandlerMock] so no real HTTP calls are
|
|
67
|
+
made. Returns both the client and the mock, so a test can inspect the requests it would have sent.
|
|
68
|
+
|
|
69
|
+
```lua
|
|
70
|
+
local client, requestMock = InfluxDBClient.newMock()
|
|
71
|
+
client:SetClientConfig({ url = "https://example.com", token = "token" })
|
|
72
|
+
|
|
73
|
+
local writeAPI = client:GetWriteAPI("org", "bucket")
|
|
74
|
+
writeAPI:QueuePoint(point)
|
|
75
|
+
writeAPI:PromiseFlush():Wait()
|
|
76
|
+
|
|
77
|
+
print(#requestMock:GetRequests()) --> 1
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
@param clientConfig InfluxDBClientConfig?
|
|
81
|
+
@return (InfluxDBClient, InfluxDBRequestHandlerMock)
|
|
82
|
+
]=]
|
|
83
|
+
function InfluxDBClient.newMock(
|
|
84
|
+
clientConfig: InfluxDBClientConfigUtils.InfluxDBClientConfig?
|
|
85
|
+
): (InfluxDBClient, InfluxDBRequestHandlerMock.InfluxDBRequestHandlerMock)
|
|
86
|
+
local requestMock = InfluxDBRequestHandlerMock.new()
|
|
87
|
+
local client = InfluxDBClient.new(clientConfig, requestMock.Handler)
|
|
88
|
+
|
|
89
|
+
return client, requestMock
|
|
90
|
+
end
|
|
91
|
+
|
|
54
92
|
--[=[
|
|
55
93
|
Sets the client config for this client
|
|
56
94
|
|
|
@@ -83,10 +121,14 @@ function InfluxDBClient.GetWriteAPI(
|
|
|
83
121
|
|
|
84
122
|
local maid = Maid.new()
|
|
85
123
|
|
|
86
|
-
local writeAPI = maid:Add(InfluxDBWriteAPI.new(org, bucket, precision))
|
|
124
|
+
local writeAPI = maid:Add(InfluxDBWriteAPI.new(org, bucket, precision, self._requestHandler))
|
|
87
125
|
|
|
88
126
|
maid:GiveTask(self._clientConfig:Observe():Subscribe(function(clientConfig)
|
|
89
|
-
|
|
127
|
+
-- The observable fires the current value immediately, which is nil until a config is set. Guard
|
|
128
|
+
-- so GetWriteAPI can be called before SetClientConfig; the later config is pushed through here.
|
|
129
|
+
if clientConfig then
|
|
130
|
+
writeAPI:SetClientConfig(clientConfig)
|
|
131
|
+
end
|
|
90
132
|
end))
|
|
91
133
|
|
|
92
134
|
maid:GiveTask(writeAPI.Destroying:Connect(function()
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class InfluxDBClient.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local InfluxDBClient = require("InfluxDBClient")
|
|
9
|
+
local InfluxDBPoint = require("InfluxDBPoint")
|
|
10
|
+
local InfluxDBRequestHandlerMock = require("InfluxDBRequestHandlerMock")
|
|
11
|
+
local Jest = require("Jest")
|
|
12
|
+
local Promise = require("Promise")
|
|
13
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
14
|
+
|
|
15
|
+
local describe = Jest.Globals.describe
|
|
16
|
+
local expect = Jest.Globals.expect
|
|
17
|
+
local it = Jest.Globals.it
|
|
18
|
+
|
|
19
|
+
local FIXED_TIMESTAMP = DateTime.fromUnixTimestampMillis(1600000000000)
|
|
20
|
+
|
|
21
|
+
local CONFIG = {
|
|
22
|
+
url = "https://example.com",
|
|
23
|
+
token = "my-token",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
local function newPoint(): InfluxDBPoint.InfluxDBPoint
|
|
27
|
+
local point = InfluxDBPoint.new("temperature")
|
|
28
|
+
point:SetTimestamp(FIXED_TIMESTAMP)
|
|
29
|
+
point:AddFloatField("value", 23.5)
|
|
30
|
+
return point
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe("InfluxDBClient.new", function()
|
|
34
|
+
it("should construct without a config", function()
|
|
35
|
+
local client = InfluxDBClient.new()
|
|
36
|
+
expect(client.ClassName).toEqual("InfluxDBClient")
|
|
37
|
+
client:Destroy()
|
|
38
|
+
end)
|
|
39
|
+
|
|
40
|
+
it("should construct with a config", function()
|
|
41
|
+
local client = InfluxDBClient.new(CONFIG)
|
|
42
|
+
expect(client.ClassName).toEqual("InfluxDBClient")
|
|
43
|
+
client:Destroy()
|
|
44
|
+
end)
|
|
45
|
+
|
|
46
|
+
it("should throw when constructed with an invalid config", function()
|
|
47
|
+
expect(function()
|
|
48
|
+
InfluxDBClient.new({ url = "https://example.com" } :: any)
|
|
49
|
+
end).toThrow("Bad clientConfig")
|
|
50
|
+
end)
|
|
51
|
+
|
|
52
|
+
it("should throw on a non-function, non-nil request handler", function()
|
|
53
|
+
expect(function()
|
|
54
|
+
InfluxDBClient.new(nil, 5 :: any)
|
|
55
|
+
end).toThrow("Bad requestHandler")
|
|
56
|
+
end)
|
|
57
|
+
end)
|
|
58
|
+
|
|
59
|
+
describe("InfluxDBClient.newMock", function()
|
|
60
|
+
it("should return a client and a request handler mock", function()
|
|
61
|
+
local client, requestMock = InfluxDBClient.newMock()
|
|
62
|
+
|
|
63
|
+
expect(client.ClassName).toEqual("InfluxDBClient")
|
|
64
|
+
expect(InfluxDBRequestHandlerMock.isInfluxDBRequestHandlerMock(requestMock)).toEqual(true)
|
|
65
|
+
|
|
66
|
+
client:Destroy()
|
|
67
|
+
end)
|
|
68
|
+
|
|
69
|
+
it("should route write API traffic through the mock instead of the network", function()
|
|
70
|
+
local client, requestMock = InfluxDBClient.newMock(CONFIG)
|
|
71
|
+
|
|
72
|
+
local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
|
|
73
|
+
writeAPI:QueuePoint(newPoint())
|
|
74
|
+
expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
|
|
75
|
+
|
|
76
|
+
expect(#requestMock:GetRequests()).toEqual(1)
|
|
77
|
+
expect((requestMock:GetLastRequest() :: any).Headers.Authorization).toEqual("Token my-token")
|
|
78
|
+
|
|
79
|
+
client:Destroy()
|
|
80
|
+
end)
|
|
81
|
+
end)
|
|
82
|
+
|
|
83
|
+
describe("InfluxDBClient.SetClientConfig", function()
|
|
84
|
+
it("should throw on an invalid config", function()
|
|
85
|
+
local client = InfluxDBClient.new()
|
|
86
|
+
|
|
87
|
+
expect(function()
|
|
88
|
+
client:SetClientConfig({ token = "my-token" } :: any)
|
|
89
|
+
end).toThrow("Bad clientConfig")
|
|
90
|
+
|
|
91
|
+
client:Destroy()
|
|
92
|
+
end)
|
|
93
|
+
end)
|
|
94
|
+
|
|
95
|
+
describe("InfluxDBClient.GetWriteAPI", function()
|
|
96
|
+
it("should throw on a non-string org", function()
|
|
97
|
+
local client = InfluxDBClient.new()
|
|
98
|
+
expect(function()
|
|
99
|
+
client:GetWriteAPI(5 :: any, "my-bucket")
|
|
100
|
+
end).toThrow("Bad org")
|
|
101
|
+
client:Destroy()
|
|
102
|
+
end)
|
|
103
|
+
|
|
104
|
+
it("should throw on a non-string bucket", function()
|
|
105
|
+
local client = InfluxDBClient.new()
|
|
106
|
+
expect(function()
|
|
107
|
+
client:GetWriteAPI("my-org", 5 :: any)
|
|
108
|
+
end).toThrow("Bad bucket")
|
|
109
|
+
client:Destroy()
|
|
110
|
+
end)
|
|
111
|
+
|
|
112
|
+
it("should throw on a non-string, non-nil precision", function()
|
|
113
|
+
local client = InfluxDBClient.new()
|
|
114
|
+
expect(function()
|
|
115
|
+
client:GetWriteAPI("my-org", "my-bucket", 5 :: any)
|
|
116
|
+
end).toThrow("Bad precision")
|
|
117
|
+
client:Destroy()
|
|
118
|
+
end)
|
|
119
|
+
|
|
120
|
+
-- Regression: GetWriteAPI must work before a config is set. Its ValueObject:Observe() subscription
|
|
121
|
+
-- fires the current value (nil) immediately; without a nil guard in GetWriteAPI this would call
|
|
122
|
+
-- writeAPI:SetClientConfig(nil) and throw "Bad clientConfig".
|
|
123
|
+
it("should return a write API", function()
|
|
124
|
+
local client = InfluxDBClient.new()
|
|
125
|
+
local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
|
|
126
|
+
expect(writeAPI.ClassName).toEqual("InfluxDBWriteAPI")
|
|
127
|
+
client:Destroy()
|
|
128
|
+
end)
|
|
129
|
+
|
|
130
|
+
it("should return the same write API for the same org and bucket", function()
|
|
131
|
+
local client = InfluxDBClient.new()
|
|
132
|
+
local first = client:GetWriteAPI("my-org", "my-bucket")
|
|
133
|
+
local second = client:GetWriteAPI("my-org", "my-bucket")
|
|
134
|
+
expect(second).toBe(first)
|
|
135
|
+
client:Destroy()
|
|
136
|
+
end)
|
|
137
|
+
|
|
138
|
+
it("should return a different write API for a different bucket", function()
|
|
139
|
+
local client = InfluxDBClient.new()
|
|
140
|
+
local first = client:GetWriteAPI("my-org", "bucket-a")
|
|
141
|
+
local second = client:GetWriteAPI("my-org", "bucket-b")
|
|
142
|
+
expect(second).never.toBe(first)
|
|
143
|
+
client:Destroy()
|
|
144
|
+
end)
|
|
145
|
+
|
|
146
|
+
it("should return a different write API for a different org", function()
|
|
147
|
+
local client = InfluxDBClient.new()
|
|
148
|
+
local first = client:GetWriteAPI("org-a", "my-bucket")
|
|
149
|
+
local second = client:GetWriteAPI("org-b", "my-bucket")
|
|
150
|
+
expect(second).never.toBe(first)
|
|
151
|
+
client:Destroy()
|
|
152
|
+
end)
|
|
153
|
+
|
|
154
|
+
it("should propagate the client config set before GetWriteAPI to the write API", function()
|
|
155
|
+
local client, requestMock = InfluxDBClient.newMock(CONFIG)
|
|
156
|
+
local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
|
|
157
|
+
|
|
158
|
+
-- If the config had not reached the write API, the flush would reject with "No client configuration".
|
|
159
|
+
writeAPI:QueuePoint(newPoint())
|
|
160
|
+
expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
|
|
161
|
+
|
|
162
|
+
expect(#requestMock:GetRequests()).toEqual(1)
|
|
163
|
+
|
|
164
|
+
client:Destroy()
|
|
165
|
+
end)
|
|
166
|
+
|
|
167
|
+
it("should propagate a client config set after GetWriteAPI to the write API", function()
|
|
168
|
+
local client, requestMock = InfluxDBClient.newMock()
|
|
169
|
+
local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
|
|
170
|
+
|
|
171
|
+
client:SetClientConfig(CONFIG)
|
|
172
|
+
|
|
173
|
+
writeAPI:QueuePoint(newPoint())
|
|
174
|
+
expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
|
|
175
|
+
|
|
176
|
+
expect(#requestMock:GetRequests()).toEqual(1)
|
|
177
|
+
|
|
178
|
+
client:Destroy()
|
|
179
|
+
end)
|
|
180
|
+
end)
|
|
181
|
+
|
|
182
|
+
describe("InfluxDBClient.PromiseFlushAll", function()
|
|
183
|
+
it("should resolve when there are no write APIs", function()
|
|
184
|
+
local client = InfluxDBClient.new()
|
|
185
|
+
|
|
186
|
+
expect(PromiseTestUtils.awaitSettled(client:PromiseFlushAll())).toEqual(true)
|
|
187
|
+
|
|
188
|
+
client:Destroy()
|
|
189
|
+
end)
|
|
190
|
+
|
|
191
|
+
it("should flush buffered points across every write API", function()
|
|
192
|
+
local client, requestMock = InfluxDBClient.newMock(CONFIG)
|
|
193
|
+
|
|
194
|
+
local apiA = client:GetWriteAPI("my-org", "bucket-a")
|
|
195
|
+
local apiB = client:GetWriteAPI("my-org", "bucket-b")
|
|
196
|
+
apiA:QueuePoint(newPoint())
|
|
197
|
+
apiB:QueuePoint(newPoint())
|
|
198
|
+
|
|
199
|
+
expect(PromiseTestUtils.awaitSettled(client:PromiseFlushAll())).toEqual(true)
|
|
200
|
+
expect(#requestMock:GetRequests()).toEqual(2)
|
|
201
|
+
|
|
202
|
+
client:Destroy()
|
|
203
|
+
end)
|
|
204
|
+
|
|
205
|
+
it("should reuse the pending flush promise while one is in flight", function()
|
|
206
|
+
local client, requestMock = InfluxDBClient.newMock(CONFIG)
|
|
207
|
+
|
|
208
|
+
-- A request that never settles keeps the flush pending, so both calls observe the same promise.
|
|
209
|
+
requestMock:SetResponder(function()
|
|
210
|
+
return Promise.new(function() end)
|
|
211
|
+
end)
|
|
212
|
+
|
|
213
|
+
local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
|
|
214
|
+
writeAPI:QueuePoint(newPoint())
|
|
215
|
+
|
|
216
|
+
local first = client:PromiseFlushAll()
|
|
217
|
+
local second = client:PromiseFlushAll()
|
|
218
|
+
|
|
219
|
+
expect(second).toBe(first)
|
|
220
|
+
|
|
221
|
+
-- Destroy cancels the in-flight request, rejecting this promise; consume it so the cancellation
|
|
222
|
+
-- does not surface as an unhandled rejection.
|
|
223
|
+
first:Catch(function() end)
|
|
224
|
+
|
|
225
|
+
client:Destroy()
|
|
226
|
+
end)
|
|
227
|
+
end)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Centralized service using serviceBag. Lets other packages share a single [InfluxDBClient] and, in
|
|
4
|
+
tests, swap the underlying request implementation for an [InfluxDBRequestHandlerMock] at the
|
|
5
|
+
ServiceBag layer -- the same pattern as [PlayerDataStoreService.SetRobloxDataStore].
|
|
6
|
+
|
|
7
|
+
```lua
|
|
8
|
+
-- Production
|
|
9
|
+
local influxDBService = serviceBag:GetService(require("InfluxDBService"))
|
|
10
|
+
-- ...after Init, before first use...
|
|
11
|
+
influxDBService:SetClientConfig({ url = "https://example.com", token = "token" })
|
|
12
|
+
|
|
13
|
+
local writeAPI = influxDBService:GetWriteAPI("org", "bucket")
|
|
14
|
+
writeAPI:QueuePoint(point)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```lua
|
|
18
|
+
-- Tests: inject a mock so nothing hits the network
|
|
19
|
+
local requestMock = InfluxDBRequestHandlerMock.new()
|
|
20
|
+
influxDBService:SetRequestHandler(requestMock.Handler)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
@server
|
|
24
|
+
@class InfluxDBService
|
|
25
|
+
]=]
|
|
26
|
+
|
|
27
|
+
local require = require(script.Parent.loader).load(script)
|
|
28
|
+
|
|
29
|
+
local InfluxDBClient = require("InfluxDBClient")
|
|
30
|
+
local InfluxDBClientConfigUtils = require("InfluxDBClientConfigUtils")
|
|
31
|
+
local InfluxDBWriteAPI = require("InfluxDBWriteAPI")
|
|
32
|
+
local Maid = require("Maid")
|
|
33
|
+
local Promise = require("Promise")
|
|
34
|
+
local ServiceBag = require("ServiceBag")
|
|
35
|
+
|
|
36
|
+
local InfluxDBService = {}
|
|
37
|
+
InfluxDBService.ServiceName = "InfluxDBService"
|
|
38
|
+
|
|
39
|
+
export type InfluxDBService = typeof(setmetatable(
|
|
40
|
+
{} :: {
|
|
41
|
+
_serviceBag: ServiceBag.ServiceBag,
|
|
42
|
+
_maid: Maid.Maid,
|
|
43
|
+
_client: InfluxDBClient.InfluxDBClient?,
|
|
44
|
+
_requestHandler: InfluxDBWriteAPI.InfluxDBRequestHandler?,
|
|
45
|
+
_pendingClientConfig: InfluxDBClientConfigUtils.InfluxDBClientConfig?,
|
|
46
|
+
},
|
|
47
|
+
{} :: typeof({ __index = InfluxDBService })
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
--[=[
|
|
51
|
+
Initializes the InfluxDBService. Should be done via [ServiceBag.Init].
|
|
52
|
+
|
|
53
|
+
@param serviceBag ServiceBag
|
|
54
|
+
]=]
|
|
55
|
+
function InfluxDBService.Init(self: InfluxDBService, serviceBag: ServiceBag.ServiceBag): ()
|
|
56
|
+
self._serviceBag = assert(serviceBag, "No serviceBag")
|
|
57
|
+
self._maid = Maid.new()
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
--[=[
|
|
61
|
+
Injects the request handler the underlying client uses to send data, instead of the real
|
|
62
|
+
[HttpPromise.request]. Pass an [InfluxDBRequestHandlerMock] handler so tests never hit the network.
|
|
63
|
+
Intended for testing; must be called before the client is first built.
|
|
64
|
+
|
|
65
|
+
@param requestHandler InfluxDBRequestHandler
|
|
66
|
+
]=]
|
|
67
|
+
function InfluxDBService.SetRequestHandler(
|
|
68
|
+
self: InfluxDBService,
|
|
69
|
+
requestHandler: InfluxDBWriteAPI.InfluxDBRequestHandler
|
|
70
|
+
): ()
|
|
71
|
+
assert(type(requestHandler) == "function", "Bad requestHandler")
|
|
72
|
+
assert(not self._client, "Already built client, cannot override requestHandler")
|
|
73
|
+
|
|
74
|
+
self._requestHandler = requestHandler
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
--[=[
|
|
78
|
+
Sets the client config used to authenticate writes. May be called before or after the client is
|
|
79
|
+
built; a later config is forwarded to the existing client.
|
|
80
|
+
|
|
81
|
+
@param clientConfig InfluxDBClientConfig
|
|
82
|
+
]=]
|
|
83
|
+
function InfluxDBService.SetClientConfig(
|
|
84
|
+
self: InfluxDBService,
|
|
85
|
+
clientConfig: InfluxDBClientConfigUtils.InfluxDBClientConfig
|
|
86
|
+
): ()
|
|
87
|
+
assert(InfluxDBClientConfigUtils.isClientConfig(clientConfig), "Bad clientConfig")
|
|
88
|
+
|
|
89
|
+
self._pendingClientConfig = clientConfig
|
|
90
|
+
|
|
91
|
+
if self._client then
|
|
92
|
+
self._client:SetClientConfig(clientConfig)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
--[=[
|
|
97
|
+
Returns the shared [InfluxDBClient], building it on first use with any injected request handler and
|
|
98
|
+
client config.
|
|
99
|
+
|
|
100
|
+
@return InfluxDBClient
|
|
101
|
+
]=]
|
|
102
|
+
function InfluxDBService.GetClient(self: InfluxDBService): InfluxDBClient.InfluxDBClient
|
|
103
|
+
if self._client then
|
|
104
|
+
return self._client
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
local client = self._maid:Add(InfluxDBClient.new(self._pendingClientConfig, self._requestHandler))
|
|
108
|
+
self._client = client
|
|
109
|
+
|
|
110
|
+
return client
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
--[=[
|
|
114
|
+
Returns the write API for the given org and bucket from the shared client.
|
|
115
|
+
|
|
116
|
+
@param org string
|
|
117
|
+
@param bucket string
|
|
118
|
+
@param precision string?
|
|
119
|
+
@return InfluxDBWriteAPI
|
|
120
|
+
]=]
|
|
121
|
+
function InfluxDBService.GetWriteAPI(
|
|
122
|
+
self: InfluxDBService,
|
|
123
|
+
org: string,
|
|
124
|
+
bucket: string,
|
|
125
|
+
precision: string?
|
|
126
|
+
): InfluxDBWriteAPI.InfluxDBWriteAPI
|
|
127
|
+
return self:GetClient():GetWriteAPI(org, bucket, precision)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
--[=[
|
|
131
|
+
Flushes every write API on the shared client. Resolves immediately if the client was never built.
|
|
132
|
+
|
|
133
|
+
@return Promise<()>
|
|
134
|
+
]=]
|
|
135
|
+
function InfluxDBService.PromiseFlushAll(self: InfluxDBService): Promise.Promise<()>
|
|
136
|
+
if not self._client then
|
|
137
|
+
return Promise.resolved()
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
return self._client:PromiseFlushAll()
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
function InfluxDBService.Destroy(self: InfluxDBService): ()
|
|
144
|
+
self._maid:DoCleaning()
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
return InfluxDBService
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class InfluxDBService.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local InfluxDBPoint = require("InfluxDBPoint")
|
|
9
|
+
local InfluxDBRequestHandlerMock = require("InfluxDBRequestHandlerMock")
|
|
10
|
+
local InfluxDBService = require("InfluxDBService")
|
|
11
|
+
local Jest = require("Jest")
|
|
12
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
13
|
+
local ServiceBag = require("ServiceBag")
|
|
14
|
+
|
|
15
|
+
local describe = Jest.Globals.describe
|
|
16
|
+
local expect = Jest.Globals.expect
|
|
17
|
+
local it = Jest.Globals.it
|
|
18
|
+
|
|
19
|
+
local FIXED_TIMESTAMP = DateTime.fromUnixTimestampMillis(1600000000000)
|
|
20
|
+
|
|
21
|
+
local CONFIG = {
|
|
22
|
+
url = "https://example.com",
|
|
23
|
+
token = "my-token",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
local function newPoint(): InfluxDBPoint.InfluxDBPoint
|
|
27
|
+
local point = InfluxDBPoint.new("temperature")
|
|
28
|
+
point:SetTimestamp(FIXED_TIMESTAMP)
|
|
29
|
+
point:AddFloatField("value", 23.5)
|
|
30
|
+
return point
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
-- Boots a service through a ServiceBag with a request mock injected between Init and Start, mirroring
|
|
34
|
+
-- how a consuming package would swap in the mock at the ServiceBag layer.
|
|
35
|
+
local function setup()
|
|
36
|
+
local serviceBag = ServiceBag.new()
|
|
37
|
+
local influxDBService = (serviceBag:GetService(InfluxDBService) :: any) :: InfluxDBService.InfluxDBService
|
|
38
|
+
serviceBag:Init()
|
|
39
|
+
|
|
40
|
+
local requestMock = InfluxDBRequestHandlerMock.new()
|
|
41
|
+
influxDBService:SetRequestHandler(requestMock.Handler)
|
|
42
|
+
|
|
43
|
+
serviceBag:Start()
|
|
44
|
+
|
|
45
|
+
return serviceBag, influxDBService, requestMock
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe("InfluxDBService.SetRequestHandler", function()
|
|
49
|
+
it("should route write traffic through the injected mock", function()
|
|
50
|
+
local serviceBag, influxDBService, requestMock = setup()
|
|
51
|
+
influxDBService:SetClientConfig(CONFIG)
|
|
52
|
+
|
|
53
|
+
local writeAPI = influxDBService:GetWriteAPI("my-org", "my-bucket")
|
|
54
|
+
writeAPI:QueuePoint(newPoint())
|
|
55
|
+
expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
|
|
56
|
+
|
|
57
|
+
expect(#requestMock:GetRequests()).toEqual(1)
|
|
58
|
+
expect((requestMock:GetLastRequest() :: any).Headers.Authorization).toEqual("Token my-token")
|
|
59
|
+
|
|
60
|
+
serviceBag:Destroy()
|
|
61
|
+
end)
|
|
62
|
+
|
|
63
|
+
it("should throw on a non-function handler", function()
|
|
64
|
+
local serviceBag = ServiceBag.new()
|
|
65
|
+
local influxDBService = (serviceBag:GetService(InfluxDBService) :: any) :: InfluxDBService.InfluxDBService
|
|
66
|
+
serviceBag:Init()
|
|
67
|
+
|
|
68
|
+
expect(function()
|
|
69
|
+
influxDBService:SetRequestHandler(5 :: any)
|
|
70
|
+
end).toThrow("Bad requestHandler")
|
|
71
|
+
|
|
72
|
+
serviceBag:Destroy()
|
|
73
|
+
end)
|
|
74
|
+
|
|
75
|
+
it("should throw once the client has been built", function()
|
|
76
|
+
local serviceBag = ServiceBag.new()
|
|
77
|
+
local influxDBService = (serviceBag:GetService(InfluxDBService) :: any) :: InfluxDBService.InfluxDBService
|
|
78
|
+
serviceBag:Init()
|
|
79
|
+
serviceBag:Start()
|
|
80
|
+
|
|
81
|
+
-- Building the client locks the seam.
|
|
82
|
+
influxDBService:GetClient()
|
|
83
|
+
|
|
84
|
+
expect(function()
|
|
85
|
+
influxDBService:SetRequestHandler(InfluxDBRequestHandlerMock.new().Handler)
|
|
86
|
+
end).toThrow("Already built client")
|
|
87
|
+
|
|
88
|
+
serviceBag:Destroy()
|
|
89
|
+
end)
|
|
90
|
+
end)
|
|
91
|
+
|
|
92
|
+
describe("InfluxDBService.SetClientConfig", function()
|
|
93
|
+
it("should throw on an invalid config", function()
|
|
94
|
+
local serviceBag, influxDBService = setup()
|
|
95
|
+
|
|
96
|
+
expect(function()
|
|
97
|
+
influxDBService:SetClientConfig({ url = "https://example.com" } :: any)
|
|
98
|
+
end).toThrow("Bad clientConfig")
|
|
99
|
+
|
|
100
|
+
serviceBag:Destroy()
|
|
101
|
+
end)
|
|
102
|
+
|
|
103
|
+
it("should apply a config set after the client is built", function()
|
|
104
|
+
local serviceBag, influxDBService, requestMock = setup()
|
|
105
|
+
|
|
106
|
+
-- Build the client and its write API before any config is set.
|
|
107
|
+
local writeAPI = influxDBService:GetWriteAPI("my-org", "my-bucket")
|
|
108
|
+
|
|
109
|
+
influxDBService:SetClientConfig(CONFIG)
|
|
110
|
+
|
|
111
|
+
writeAPI:QueuePoint(newPoint())
|
|
112
|
+
expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
|
|
113
|
+
|
|
114
|
+
expect(#requestMock:GetRequests()).toEqual(1)
|
|
115
|
+
|
|
116
|
+
serviceBag:Destroy()
|
|
117
|
+
end)
|
|
118
|
+
end)
|
|
119
|
+
|
|
120
|
+
describe("InfluxDBService.GetClient", function()
|
|
121
|
+
it("should return the same client across calls", function()
|
|
122
|
+
local serviceBag, influxDBService = setup()
|
|
123
|
+
|
|
124
|
+
expect(influxDBService:GetClient()).toBe(influxDBService:GetClient())
|
|
125
|
+
|
|
126
|
+
serviceBag:Destroy()
|
|
127
|
+
end)
|
|
128
|
+
end)
|
|
129
|
+
|
|
130
|
+
describe("InfluxDBService.PromiseFlushAll", function()
|
|
131
|
+
it("should resolve when the client was never built", function()
|
|
132
|
+
local serviceBag, influxDBService = setup()
|
|
133
|
+
|
|
134
|
+
expect(PromiseTestUtils.awaitSettled(influxDBService:PromiseFlushAll())).toEqual(true)
|
|
135
|
+
|
|
136
|
+
serviceBag:Destroy()
|
|
137
|
+
end)
|
|
138
|
+
|
|
139
|
+
it("should flush buffered points once the client is built", function()
|
|
140
|
+
local serviceBag, influxDBService, requestMock = setup()
|
|
141
|
+
influxDBService:SetClientConfig(CONFIG)
|
|
142
|
+
|
|
143
|
+
local writeAPI = influxDBService:GetWriteAPI("my-org", "my-bucket")
|
|
144
|
+
writeAPI:QueuePoint(newPoint())
|
|
145
|
+
|
|
146
|
+
expect(PromiseTestUtils.awaitSettled(influxDBService:PromiseFlushAll())).toEqual(true)
|
|
147
|
+
expect(#requestMock:GetRequests()).toEqual(1)
|
|
148
|
+
|
|
149
|
+
serviceBag:Destroy()
|
|
150
|
+
end)
|
|
151
|
+
end)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
In-memory stand-in for [HttpPromise.request] used to test InfluxDB writes without making real HTTP
|
|
4
|
+
calls. Inject its [InfluxDBRequestHandlerMock.Handler] wherever an [InfluxDBRequestHandler] is
|
|
5
|
+
expected -- into [InfluxDBClient.new], via [InfluxDBService.SetRequestHandler], or use
|
|
6
|
+
[InfluxDBClient.newMock], which wires one up for you.
|
|
7
|
+
|
|
8
|
+
Every request handed to the mock is recorded for inspection. By default each request resolves with a
|
|
9
|
+
204 success response; call [InfluxDBRequestHandlerMock.SetResponder] to control the response (for
|
|
10
|
+
example to simulate a failure).
|
|
11
|
+
|
|
12
|
+
```lua
|
|
13
|
+
local requestMock = InfluxDBRequestHandlerMock.new()
|
|
14
|
+
local client = InfluxDBClient.new(config, requestMock.Handler)
|
|
15
|
+
-- ...write some points...
|
|
16
|
+
print(#requestMock:GetRequests())
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
@server
|
|
20
|
+
@class InfluxDBRequestHandlerMock
|
|
21
|
+
]=]
|
|
22
|
+
|
|
23
|
+
local require = require(script.Parent.loader).load(script)
|
|
24
|
+
|
|
25
|
+
local HttpPromise = require("HttpPromise")
|
|
26
|
+
local Promise = require("Promise")
|
|
27
|
+
|
|
28
|
+
local InfluxDBRequestHandlerMock = {}
|
|
29
|
+
InfluxDBRequestHandlerMock.ClassName = "InfluxDBRequestHandlerMock"
|
|
30
|
+
InfluxDBRequestHandlerMock.__index = InfluxDBRequestHandlerMock
|
|
31
|
+
|
|
32
|
+
--[=[
|
|
33
|
+
A responder controls how the mock answers a request, so a test can resolve or reject however it needs.
|
|
34
|
+
|
|
35
|
+
@type Responder (request: HttpPromise.HTTPRequest) -> Promise<HttpPromise.HTTPResponse>
|
|
36
|
+
@within InfluxDBRequestHandlerMock
|
|
37
|
+
]=]
|
|
38
|
+
export type Responder = (request: HttpPromise.HTTPRequest) -> Promise.Promise<HttpPromise.HTTPResponse>
|
|
39
|
+
|
|
40
|
+
export type InfluxDBRequestHandlerMock = typeof(setmetatable(
|
|
41
|
+
{} :: {
|
|
42
|
+
-- The request handler function to inject; recording is done through it.
|
|
43
|
+
Handler: Responder,
|
|
44
|
+
_requests: { HttpPromise.HTTPRequest },
|
|
45
|
+
_responder: Responder?,
|
|
46
|
+
},
|
|
47
|
+
{} :: typeof({ __index = InfluxDBRequestHandlerMock })
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
local function defaultResponse(): HttpPromise.HTTPResponse
|
|
51
|
+
return {
|
|
52
|
+
Success = true,
|
|
53
|
+
StatusCode = 204,
|
|
54
|
+
StatusMessage = "No Content",
|
|
55
|
+
Headers = {},
|
|
56
|
+
Body = "",
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
--[=[
|
|
61
|
+
Returns whether the given value is an [InfluxDBRequestHandlerMock].
|
|
62
|
+
|
|
63
|
+
@param value any
|
|
64
|
+
@return boolean
|
|
65
|
+
]=]
|
|
66
|
+
function InfluxDBRequestHandlerMock.isInfluxDBRequestHandlerMock(value: any): boolean
|
|
67
|
+
return type(value) == "table" and getmetatable(value) == InfluxDBRequestHandlerMock
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
--[=[
|
|
71
|
+
Constructs a new InfluxDBRequestHandlerMock.
|
|
72
|
+
|
|
73
|
+
@return InfluxDBRequestHandlerMock
|
|
74
|
+
]=]
|
|
75
|
+
function InfluxDBRequestHandlerMock.new(): InfluxDBRequestHandlerMock
|
|
76
|
+
local self: InfluxDBRequestHandlerMock = setmetatable({} :: any, InfluxDBRequestHandlerMock)
|
|
77
|
+
|
|
78
|
+
self._requests = {}
|
|
79
|
+
self._responder = nil
|
|
80
|
+
|
|
81
|
+
-- Bound closure so it can be passed wherever a plain [InfluxDBRequestHandler] function is expected.
|
|
82
|
+
self.Handler = function(request)
|
|
83
|
+
return self:_handleRequest(request)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
return self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
function InfluxDBRequestHandlerMock._handleRequest(
|
|
90
|
+
self: InfluxDBRequestHandlerMock,
|
|
91
|
+
request: HttpPromise.HTTPRequest
|
|
92
|
+
): Promise.Promise<HttpPromise.HTTPResponse>
|
|
93
|
+
table.insert(self._requests, request)
|
|
94
|
+
|
|
95
|
+
if self._responder then
|
|
96
|
+
return self._responder(request)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
return Promise.resolved(defaultResponse())
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
--[=[
|
|
103
|
+
Sets a responder invoked for each subsequent request, letting a test resolve or reject however it
|
|
104
|
+
needs. Pass nil to restore the default 204 success response.
|
|
105
|
+
|
|
106
|
+
@param responder Responder?
|
|
107
|
+
]=]
|
|
108
|
+
function InfluxDBRequestHandlerMock.SetResponder(self: InfluxDBRequestHandlerMock, responder: Responder?): ()
|
|
109
|
+
assert(type(responder) == "function" or responder == nil, "Bad responder")
|
|
110
|
+
|
|
111
|
+
self._responder = responder
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
--[=[
|
|
115
|
+
Returns every request handed to the mock, in order.
|
|
116
|
+
|
|
117
|
+
@return { HttpPromise.HTTPRequest }
|
|
118
|
+
]=]
|
|
119
|
+
function InfluxDBRequestHandlerMock.GetRequests(self: InfluxDBRequestHandlerMock): { HttpPromise.HTTPRequest }
|
|
120
|
+
return self._requests
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
--[=[
|
|
124
|
+
Returns the most recent request handed to the mock, or nil if there have been none.
|
|
125
|
+
|
|
126
|
+
@return HttpPromise.HTTPRequest?
|
|
127
|
+
]=]
|
|
128
|
+
function InfluxDBRequestHandlerMock.GetLastRequest(self: InfluxDBRequestHandlerMock): HttpPromise.HTTPRequest?
|
|
129
|
+
return self._requests[#self._requests]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
return InfluxDBRequestHandlerMock
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class InfluxDBRequestHandlerMock.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local InfluxDBRequestHandlerMock = require("InfluxDBRequestHandlerMock")
|
|
9
|
+
local Jest = require("Jest")
|
|
10
|
+
local Promise = require("Promise")
|
|
11
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
12
|
+
|
|
13
|
+
local describe = Jest.Globals.describe
|
|
14
|
+
local expect = Jest.Globals.expect
|
|
15
|
+
local it = Jest.Globals.it
|
|
16
|
+
|
|
17
|
+
local function request(url: string): any
|
|
18
|
+
return {
|
|
19
|
+
Method = "POST",
|
|
20
|
+
Url = url,
|
|
21
|
+
Headers = {},
|
|
22
|
+
Body = "",
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe("InfluxDBRequestHandlerMock.isInfluxDBRequestHandlerMock", function()
|
|
27
|
+
it("should be true for a mock", function()
|
|
28
|
+
expect(InfluxDBRequestHandlerMock.isInfluxDBRequestHandlerMock(InfluxDBRequestHandlerMock.new())).toEqual(true)
|
|
29
|
+
end)
|
|
30
|
+
|
|
31
|
+
it("should be false for other values", function()
|
|
32
|
+
expect(InfluxDBRequestHandlerMock.isInfluxDBRequestHandlerMock(nil)).toEqual(false)
|
|
33
|
+
expect(InfluxDBRequestHandlerMock.isInfluxDBRequestHandlerMock({})).toEqual(false)
|
|
34
|
+
expect(InfluxDBRequestHandlerMock.isInfluxDBRequestHandlerMock(5)).toEqual(false)
|
|
35
|
+
end)
|
|
36
|
+
end)
|
|
37
|
+
|
|
38
|
+
describe("InfluxDBRequestHandlerMock.Handler", function()
|
|
39
|
+
it("should record each request in order", function()
|
|
40
|
+
local mock = InfluxDBRequestHandlerMock.new()
|
|
41
|
+
|
|
42
|
+
mock.Handler(request("https://a.example.com"))
|
|
43
|
+
mock.Handler(request("https://b.example.com"))
|
|
44
|
+
|
|
45
|
+
local requests = mock:GetRequests()
|
|
46
|
+
expect(#requests).toEqual(2)
|
|
47
|
+
expect(requests[1].Url).toEqual("https://a.example.com")
|
|
48
|
+
expect(requests[2].Url).toEqual("https://b.example.com")
|
|
49
|
+
end)
|
|
50
|
+
|
|
51
|
+
it("should resolve with a default success response", function()
|
|
52
|
+
local mock = InfluxDBRequestHandlerMock.new()
|
|
53
|
+
|
|
54
|
+
local outcome, payload = PromiseTestUtils.awaitOutcome(mock.Handler(request("https://a.example.com")))
|
|
55
|
+
expect(outcome).toEqual("resolved")
|
|
56
|
+
expect(payload.Success).toEqual(true)
|
|
57
|
+
expect(payload.StatusCode).toEqual(204)
|
|
58
|
+
end)
|
|
59
|
+
end)
|
|
60
|
+
|
|
61
|
+
describe("InfluxDBRequestHandlerMock.GetLastRequest", function()
|
|
62
|
+
it("should return nil before any request", function()
|
|
63
|
+
local mock = InfluxDBRequestHandlerMock.new()
|
|
64
|
+
expect(mock:GetLastRequest()).toEqual(nil)
|
|
65
|
+
end)
|
|
66
|
+
|
|
67
|
+
it("should return the most recent request", function()
|
|
68
|
+
local mock = InfluxDBRequestHandlerMock.new()
|
|
69
|
+
mock.Handler(request("https://a.example.com"))
|
|
70
|
+
mock.Handler(request("https://b.example.com"))
|
|
71
|
+
|
|
72
|
+
expect((mock:GetLastRequest() :: any).Url).toEqual("https://b.example.com")
|
|
73
|
+
end)
|
|
74
|
+
end)
|
|
75
|
+
|
|
76
|
+
describe("InfluxDBRequestHandlerMock.SetResponder", function()
|
|
77
|
+
it("should route requests through a custom responder", function()
|
|
78
|
+
local mock = InfluxDBRequestHandlerMock.new()
|
|
79
|
+
mock:SetResponder(function()
|
|
80
|
+
return Promise.rejected("boom")
|
|
81
|
+
end)
|
|
82
|
+
|
|
83
|
+
local outcome, payload = PromiseTestUtils.awaitOutcome(mock.Handler(request("https://a.example.com")))
|
|
84
|
+
expect(outcome).toEqual("rejected")
|
|
85
|
+
expect(payload).toEqual("boom")
|
|
86
|
+
-- The request is still recorded even when the responder rejects.
|
|
87
|
+
expect(#mock:GetRequests()).toEqual(1)
|
|
88
|
+
end)
|
|
89
|
+
|
|
90
|
+
it("should restore the default response when set back to nil", function()
|
|
91
|
+
local mock = InfluxDBRequestHandlerMock.new()
|
|
92
|
+
mock:SetResponder(function()
|
|
93
|
+
return Promise.rejected("boom")
|
|
94
|
+
end)
|
|
95
|
+
mock:SetResponder(nil)
|
|
96
|
+
|
|
97
|
+
local outcome = PromiseTestUtils.awaitOutcome(mock.Handler(request("https://a.example.com")))
|
|
98
|
+
expect(outcome).toEqual("resolved")
|
|
99
|
+
end)
|
|
100
|
+
|
|
101
|
+
it("should throw on a non-function, non-nil responder", function()
|
|
102
|
+
local mock = InfluxDBRequestHandlerMock.new()
|
|
103
|
+
expect(function()
|
|
104
|
+
mock:SetResponder(5 :: any)
|
|
105
|
+
end).toThrow("Bad responder")
|
|
106
|
+
end)
|
|
107
|
+
end)
|
|
@@ -24,6 +24,15 @@ local InfluxDBWriteAPI = setmetatable({}, BaseObject)
|
|
|
24
24
|
InfluxDBWriteAPI.ClassName = "InfluxDBWriteAPI"
|
|
25
25
|
InfluxDBWriteAPI.__index = InfluxDBWriteAPI
|
|
26
26
|
|
|
27
|
+
--[=[
|
|
28
|
+
Function used to actually send a request. Defaults to [HttpPromise.request], but can be injected so
|
|
29
|
+
tests can stand in a mock (see [InfluxDBRequestHandlerMock]) instead of making real HTTP calls.
|
|
30
|
+
|
|
31
|
+
@type InfluxDBRequestHandler (request: HttpPromise.HTTPRequest) -> Promise<HttpPromise.HTTPResponse>
|
|
32
|
+
@within InfluxDBWriteAPI
|
|
33
|
+
]=]
|
|
34
|
+
export type InfluxDBRequestHandler = (request: HttpPromise.HTTPRequest) -> Promise.Promise<HttpPromise.HTTPResponse>
|
|
35
|
+
|
|
27
36
|
export type InfluxDBWriteAPI =
|
|
28
37
|
typeof(setmetatable(
|
|
29
38
|
{} :: {
|
|
@@ -38,6 +47,7 @@ export type InfluxDBWriteAPI =
|
|
|
38
47
|
_pointSettings: InfluxDBPointSettings.InfluxDBPointSettings,
|
|
39
48
|
_writeOptions: InfluxDBWriteOptionUtils.InfluxDBWriteOptions,
|
|
40
49
|
_writeBuffer: InfluxDBWriteBuffer.InfluxDBWriteBuffer,
|
|
50
|
+
_requestHandler: InfluxDBRequestHandler,
|
|
41
51
|
},
|
|
42
52
|
{} :: typeof({ __index = InfluxDBWriteAPI })
|
|
43
53
|
))
|
|
@@ -49,14 +59,23 @@ export type InfluxDBWriteAPI =
|
|
|
49
59
|
@param org string
|
|
50
60
|
@param bucket string
|
|
51
61
|
@param precision string?
|
|
62
|
+
@param requestHandler InfluxDBRequestHandler? -- Defaults to [HttpPromise.request]. Inject a mock to avoid real HTTP.
|
|
52
63
|
@return InfluxDBWriteAPI
|
|
53
64
|
]=]
|
|
54
|
-
function InfluxDBWriteAPI.new(
|
|
65
|
+
function InfluxDBWriteAPI.new(
|
|
66
|
+
org: string,
|
|
67
|
+
bucket: string,
|
|
68
|
+
precision: string?,
|
|
69
|
+
requestHandler: InfluxDBRequestHandler?
|
|
70
|
+
): InfluxDBWriteAPI
|
|
55
71
|
local self: InfluxDBWriteAPI = setmetatable(BaseObject.new() :: any, InfluxDBWriteAPI)
|
|
56
72
|
|
|
57
73
|
assert(type(org) == "string", "Bad org")
|
|
58
74
|
assert(type(bucket) == "string", "Bad bucket")
|
|
59
75
|
assert(type(precision) == "string" or precision == nil, "Bad precision")
|
|
76
|
+
assert(type(requestHandler) == "function" or requestHandler == nil, "Bad requestHandler")
|
|
77
|
+
|
|
78
|
+
self._requestHandler = requestHandler or (HttpPromise.request :: any)
|
|
60
79
|
|
|
61
80
|
self._clientConfig = self._maid:Add(ValueObject.new(nil))
|
|
62
81
|
|
|
@@ -193,7 +212,7 @@ function InfluxDBWriteAPI._promiseSendBatch(self: InfluxDBWriteAPI, toSend: { st
|
|
|
193
212
|
end
|
|
194
213
|
|
|
195
214
|
return self._maid
|
|
196
|
-
:GivePromise(
|
|
215
|
+
:GivePromise(self._requestHandler(request))
|
|
197
216
|
:Then(function(result)
|
|
198
217
|
if result.Success then
|
|
199
218
|
if self.Destroy then
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class InfluxDBWriteAPI.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local InfluxDBPoint = require("InfluxDBPoint")
|
|
9
|
+
local InfluxDBRequestHandlerMock = require("InfluxDBRequestHandlerMock")
|
|
10
|
+
local InfluxDBWriteAPI = require("InfluxDBWriteAPI")
|
|
11
|
+
local Jest = require("Jest")
|
|
12
|
+
local Promise = require("Promise")
|
|
13
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
14
|
+
|
|
15
|
+
local describe = Jest.Globals.describe
|
|
16
|
+
local expect = Jest.Globals.expect
|
|
17
|
+
local it = Jest.Globals.it
|
|
18
|
+
|
|
19
|
+
-- A fixed timestamp keeps the emitted line protocol (and therefore the request body) deterministic.
|
|
20
|
+
local FIXED_TIMESTAMP = DateTime.fromUnixTimestampMillis(1600000000000)
|
|
21
|
+
|
|
22
|
+
local function newPoint(): InfluxDBPoint.InfluxDBPoint
|
|
23
|
+
local point = InfluxDBPoint.new("temperature")
|
|
24
|
+
point:SetTimestamp(FIXED_TIMESTAMP)
|
|
25
|
+
point:AddFloatField("value", 23.5)
|
|
26
|
+
return point
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
-- Line protocol produced by newPoint() with default (empty) point settings.
|
|
30
|
+
local POINT_LINE = "temperature value=23.5 1600000000000"
|
|
31
|
+
|
|
32
|
+
-- Builds a write API wired to a request mock (so nothing hits the network) with a client config set.
|
|
33
|
+
local function newConfiguredAPI(configOverrides: { [string]: any }?)
|
|
34
|
+
local mock = InfluxDBRequestHandlerMock.new()
|
|
35
|
+
local api = InfluxDBWriteAPI.new("my-org", "my-bucket", nil, mock.Handler)
|
|
36
|
+
|
|
37
|
+
local config = {
|
|
38
|
+
url = "https://example.com",
|
|
39
|
+
token = "my-token",
|
|
40
|
+
}
|
|
41
|
+
for key, value in configOverrides or ({} :: { [string]: any }) do
|
|
42
|
+
config[key] = value
|
|
43
|
+
end
|
|
44
|
+
api:SetClientConfig(config)
|
|
45
|
+
|
|
46
|
+
return api, mock
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe("InfluxDBWriteAPI.new", function()
|
|
50
|
+
it("should default the precision to ms in the write url", function()
|
|
51
|
+
local api, mock = newConfiguredAPI()
|
|
52
|
+
api:QueuePoint(newPoint())
|
|
53
|
+
expect(PromiseTestUtils.awaitSettled(api:PromiseFlush())).toEqual(true)
|
|
54
|
+
|
|
55
|
+
expect((mock:GetLastRequest() :: any).Url).toEqual(
|
|
56
|
+
"https://example.com/api/v2/write?org=my-org&bucket=my-bucket&precision=ms"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
api:Destroy()
|
|
60
|
+
end)
|
|
61
|
+
|
|
62
|
+
it("should use a provided precision in the write url", function()
|
|
63
|
+
local mock = InfluxDBRequestHandlerMock.new()
|
|
64
|
+
local api = InfluxDBWriteAPI.new("my-org", "my-bucket", "ns", mock.Handler)
|
|
65
|
+
api:SetClientConfig({ url = "https://example.com", token = "my-token" })
|
|
66
|
+
api:QueuePoint(newPoint())
|
|
67
|
+
expect(PromiseTestUtils.awaitSettled(api:PromiseFlush())).toEqual(true)
|
|
68
|
+
|
|
69
|
+
expect((mock:GetLastRequest() :: any).Url).toEqual(
|
|
70
|
+
"https://example.com/api/v2/write?org=my-org&bucket=my-bucket&precision=ns"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
api:Destroy()
|
|
74
|
+
end)
|
|
75
|
+
|
|
76
|
+
it("should throw on a non-string org", function()
|
|
77
|
+
expect(function()
|
|
78
|
+
InfluxDBWriteAPI.new(5 :: any, "my-bucket")
|
|
79
|
+
end).toThrow("Bad org")
|
|
80
|
+
end)
|
|
81
|
+
|
|
82
|
+
it("should throw on a non-string bucket", function()
|
|
83
|
+
expect(function()
|
|
84
|
+
InfluxDBWriteAPI.new("my-org", 5 :: any)
|
|
85
|
+
end).toThrow("Bad bucket")
|
|
86
|
+
end)
|
|
87
|
+
|
|
88
|
+
it("should throw on a non-string, non-nil precision", function()
|
|
89
|
+
expect(function()
|
|
90
|
+
InfluxDBWriteAPI.new("my-org", "my-bucket", 5 :: any)
|
|
91
|
+
end).toThrow("Bad precision")
|
|
92
|
+
end)
|
|
93
|
+
|
|
94
|
+
it("should throw on a non-function, non-nil request handler", function()
|
|
95
|
+
expect(function()
|
|
96
|
+
InfluxDBWriteAPI.new("my-org", "my-bucket", nil, 5 :: any)
|
|
97
|
+
end).toThrow("Bad requestHandler")
|
|
98
|
+
end)
|
|
99
|
+
end)
|
|
100
|
+
|
|
101
|
+
describe("InfluxDBWriteAPI.SetClientConfig", function()
|
|
102
|
+
it("should throw on an invalid config", function()
|
|
103
|
+
local api = InfluxDBWriteAPI.new("my-org", "my-bucket")
|
|
104
|
+
|
|
105
|
+
expect(function()
|
|
106
|
+
api:SetClientConfig({ url = "https://example.com" } :: any)
|
|
107
|
+
end).toThrow("Bad clientConfig")
|
|
108
|
+
|
|
109
|
+
api:Destroy()
|
|
110
|
+
end)
|
|
111
|
+
end)
|
|
112
|
+
|
|
113
|
+
describe("InfluxDBWriteAPI.SetPrintDebugWriteEnabled", function()
|
|
114
|
+
it("should throw on a non-boolean value", function()
|
|
115
|
+
local api = InfluxDBWriteAPI.new("my-org", "my-bucket")
|
|
116
|
+
|
|
117
|
+
expect(function()
|
|
118
|
+
api:SetPrintDebugWriteEnabled("yes" :: any)
|
|
119
|
+
end).toThrow("Bad printDebugEnabled")
|
|
120
|
+
|
|
121
|
+
api:Destroy()
|
|
122
|
+
end)
|
|
123
|
+
end)
|
|
124
|
+
|
|
125
|
+
describe("InfluxDBWriteAPI.QueuePoint", function()
|
|
126
|
+
it("should throw on a non-point", function()
|
|
127
|
+
local api = InfluxDBWriteAPI.new("my-org", "my-bucket")
|
|
128
|
+
|
|
129
|
+
expect(function()
|
|
130
|
+
api:QueuePoint({} :: any)
|
|
131
|
+
end).toThrow("Bad point")
|
|
132
|
+
|
|
133
|
+
api:Destroy()
|
|
134
|
+
end)
|
|
135
|
+
|
|
136
|
+
it("should not send a request until flushed", function()
|
|
137
|
+
local api, mock = newConfiguredAPI()
|
|
138
|
+
api:QueuePoint(newPoint())
|
|
139
|
+
|
|
140
|
+
expect(#mock:GetRequests()).toEqual(0)
|
|
141
|
+
|
|
142
|
+
api:Destroy()
|
|
143
|
+
end)
|
|
144
|
+
end)
|
|
145
|
+
|
|
146
|
+
describe("InfluxDBWriteAPI.QueuePoints", function()
|
|
147
|
+
it("should throw on a non-table", function()
|
|
148
|
+
local api = InfluxDBWriteAPI.new("my-org", "my-bucket")
|
|
149
|
+
|
|
150
|
+
expect(function()
|
|
151
|
+
api:QueuePoints("nope" :: any)
|
|
152
|
+
end).toThrow("Bad points")
|
|
153
|
+
|
|
154
|
+
api:Destroy()
|
|
155
|
+
end)
|
|
156
|
+
|
|
157
|
+
it("should throw on a non-point entry in the list", function()
|
|
158
|
+
local api = InfluxDBWriteAPI.new("my-org", "my-bucket")
|
|
159
|
+
|
|
160
|
+
expect(function()
|
|
161
|
+
api:QueuePoints({ {} :: any })
|
|
162
|
+
end).toThrow("Bad point")
|
|
163
|
+
|
|
164
|
+
api:Destroy()
|
|
165
|
+
end)
|
|
166
|
+
|
|
167
|
+
it("should batch every queued point into a single newline-joined body", function()
|
|
168
|
+
local api, mock = newConfiguredAPI()
|
|
169
|
+
api:QueuePoints({ newPoint(), newPoint() })
|
|
170
|
+
expect(PromiseTestUtils.awaitSettled(api:PromiseFlush())).toEqual(true)
|
|
171
|
+
|
|
172
|
+
expect(#mock:GetRequests()).toEqual(1)
|
|
173
|
+
expect((mock:GetLastRequest() :: any).Body).toEqual(POINT_LINE .. "\n" .. POINT_LINE)
|
|
174
|
+
|
|
175
|
+
api:Destroy()
|
|
176
|
+
end)
|
|
177
|
+
end)
|
|
178
|
+
|
|
179
|
+
describe("InfluxDBWriteAPI.PromiseFlush", function()
|
|
180
|
+
it("should reject with no client configuration when none is set", function()
|
|
181
|
+
local mock = InfluxDBRequestHandlerMock.new()
|
|
182
|
+
local api = InfluxDBWriteAPI.new("my-org", "my-bucket", nil, mock.Handler)
|
|
183
|
+
api:QueuePoint(newPoint())
|
|
184
|
+
|
|
185
|
+
local outcome, payload = PromiseTestUtils.awaitOutcome(api:PromiseFlush())
|
|
186
|
+
expect(outcome).toEqual("rejected")
|
|
187
|
+
expect(payload).toEqual("No client configuration")
|
|
188
|
+
-- The batch is abandoned before a request is ever built.
|
|
189
|
+
expect(#mock:GetRequests()).toEqual(0)
|
|
190
|
+
|
|
191
|
+
api:Destroy()
|
|
192
|
+
end)
|
|
193
|
+
|
|
194
|
+
it("should send the buffered line protocol as the request body", function()
|
|
195
|
+
local api, mock = newConfiguredAPI()
|
|
196
|
+
api:QueuePoint(newPoint())
|
|
197
|
+
expect(PromiseTestUtils.awaitSettled(api:PromiseFlush())).toEqual(true)
|
|
198
|
+
|
|
199
|
+
expect((mock:GetLastRequest() :: any).Method).toEqual("POST")
|
|
200
|
+
expect((mock:GetLastRequest() :: any).Body).toEqual(POINT_LINE)
|
|
201
|
+
|
|
202
|
+
api:Destroy()
|
|
203
|
+
end)
|
|
204
|
+
|
|
205
|
+
it("should send a Token-prefixed authorization header for a string token", function()
|
|
206
|
+
local api, mock = newConfiguredAPI()
|
|
207
|
+
api:QueuePoint(newPoint())
|
|
208
|
+
expect(PromiseTestUtils.awaitSettled(api:PromiseFlush())).toEqual(true)
|
|
209
|
+
|
|
210
|
+
expect((mock:GetLastRequest() :: any).Headers.Authorization).toEqual("Token my-token")
|
|
211
|
+
|
|
212
|
+
api:Destroy()
|
|
213
|
+
end)
|
|
214
|
+
|
|
215
|
+
it("should strip a trailing slash from the configured url", function()
|
|
216
|
+
local api, mock = newConfiguredAPI({ url = "https://example.com/" })
|
|
217
|
+
api:QueuePoint(newPoint())
|
|
218
|
+
expect(PromiseTestUtils.awaitSettled(api:PromiseFlush())).toEqual(true)
|
|
219
|
+
|
|
220
|
+
expect((mock:GetLastRequest() :: any).Url).toEqual(
|
|
221
|
+
"https://example.com/api/v2/write?org=my-org&bucket=my-bucket&precision=ms"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
api:Destroy()
|
|
225
|
+
end)
|
|
226
|
+
|
|
227
|
+
it("should resolve and fire RequestFinished on a successful response", function()
|
|
228
|
+
local api, mock = newConfiguredAPI()
|
|
229
|
+
|
|
230
|
+
local response = {
|
|
231
|
+
Success = true,
|
|
232
|
+
StatusCode = 204,
|
|
233
|
+
StatusMessage = "No Content",
|
|
234
|
+
Headers = {},
|
|
235
|
+
Body = "",
|
|
236
|
+
}
|
|
237
|
+
mock:SetResponder(function()
|
|
238
|
+
return Promise.resolved(response)
|
|
239
|
+
end)
|
|
240
|
+
|
|
241
|
+
local finished = {}
|
|
242
|
+
api.RequestFinished:Connect(function(result)
|
|
243
|
+
table.insert(finished, result)
|
|
244
|
+
end)
|
|
245
|
+
|
|
246
|
+
api:QueuePoint(newPoint())
|
|
247
|
+
local outcome = PromiseTestUtils.awaitOutcome(api:PromiseFlush())
|
|
248
|
+
|
|
249
|
+
expect(outcome).toEqual("resolved")
|
|
250
|
+
expect(finished[1]).toBe(response)
|
|
251
|
+
|
|
252
|
+
api:Destroy()
|
|
253
|
+
end)
|
|
254
|
+
|
|
255
|
+
it("should reject with the parsed InfluxDB error when the body is a known error", function()
|
|
256
|
+
local api, mock = newConfiguredAPI()
|
|
257
|
+
mock:SetResponder(function()
|
|
258
|
+
return Promise.rejected({
|
|
259
|
+
Success = false,
|
|
260
|
+
StatusCode = 401,
|
|
261
|
+
StatusMessage = "Unauthorized",
|
|
262
|
+
Headers = {},
|
|
263
|
+
Body = '{"code":"unauthorized","message":"token required"}',
|
|
264
|
+
})
|
|
265
|
+
end)
|
|
266
|
+
|
|
267
|
+
api:QueuePoint(newPoint())
|
|
268
|
+
|
|
269
|
+
local outcome, payload = PromiseTestUtils.awaitOutcome(api:PromiseFlush())
|
|
270
|
+
expect(outcome).toEqual("rejected")
|
|
271
|
+
expect(payload.code).toEqual("unauthorized")
|
|
272
|
+
expect(payload.message).toEqual("token required")
|
|
273
|
+
|
|
274
|
+
api:Destroy()
|
|
275
|
+
end)
|
|
276
|
+
|
|
277
|
+
it("should reject with a formatted string when the error body is not a known error", function()
|
|
278
|
+
local api, mock = newConfiguredAPI()
|
|
279
|
+
mock:SetResponder(function()
|
|
280
|
+
return Promise.rejected({
|
|
281
|
+
Success = false,
|
|
282
|
+
StatusCode = 500,
|
|
283
|
+
StatusMessage = "Internal Server Error",
|
|
284
|
+
Headers = {},
|
|
285
|
+
Body = "not json",
|
|
286
|
+
})
|
|
287
|
+
end)
|
|
288
|
+
|
|
289
|
+
api:QueuePoint(newPoint())
|
|
290
|
+
|
|
291
|
+
local outcome, payload = PromiseTestUtils.awaitOutcome(api:PromiseFlush())
|
|
292
|
+
expect(outcome).toEqual("rejected")
|
|
293
|
+
expect(type(payload)).toEqual("string")
|
|
294
|
+
expect(string.find(payload, "500", 1, true) ~= nil).toEqual(true)
|
|
295
|
+
|
|
296
|
+
api:Destroy()
|
|
297
|
+
end)
|
|
298
|
+
|
|
299
|
+
it("should reject with the raw error when it is not an http response", function()
|
|
300
|
+
local api, mock = newConfiguredAPI()
|
|
301
|
+
mock:SetResponder(function()
|
|
302
|
+
return Promise.rejected("connection refused")
|
|
303
|
+
end)
|
|
304
|
+
|
|
305
|
+
api:QueuePoint(newPoint())
|
|
306
|
+
|
|
307
|
+
local outcome, payload = PromiseTestUtils.awaitOutcome(api:PromiseFlush())
|
|
308
|
+
expect(outcome).toEqual("rejected")
|
|
309
|
+
expect(payload).toEqual("connection refused")
|
|
310
|
+
|
|
311
|
+
api:Destroy()
|
|
312
|
+
end)
|
|
313
|
+
|
|
314
|
+
it("should resolve without a request when nothing is buffered", function()
|
|
315
|
+
local api, mock = newConfiguredAPI()
|
|
316
|
+
|
|
317
|
+
expect(PromiseTestUtils.awaitSettled(api:PromiseFlush())).toEqual(true)
|
|
318
|
+
expect(#mock:GetRequests()).toEqual(0)
|
|
319
|
+
|
|
320
|
+
api:Destroy()
|
|
321
|
+
end)
|
|
322
|
+
end)
|