@quenty/influxdbclient 7.38.0 → 7.40.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 CHANGED
@@ -3,6 +3,18 @@
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.40.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/influxdbclient@7.39.0...@quenty/influxdbclient@7.40.0) (2026-07-23)
7
+
8
+ ### Features
9
+
10
+ - Add baseline player-mock and support across Nevermore for mocked players. ([567d121](https://github.com/Quenty/NevermoreEngine/commit/567d121ffc014b42391554088189a1a6296dda83))
11
+
12
+ # [7.39.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/influxdbclient@7.38.0...@quenty/influxdbclient@7.39.0) (2026-07-22)
13
+
14
+ ### Features
15
+
16
+ - Add tests to InfluxDBClient package ([c2b138f](https://github.com/Quenty/NevermoreEngine/commit/c2b138f0c0ed050dd6cb3541f644b766b5db6b10))
17
+
6
18
  # [7.38.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/influxdbclient@7.37.0...@quenty/influxdbclient@7.38.0) (2026-07-21)
7
19
 
8
20
  **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.38.0",
3
+ "version": "7.40.0",
4
4
  "description": "Provides a Roblox Lua InfluxDB client",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -28,24 +28,24 @@
28
28
  "Quenty"
29
29
  ],
30
30
  "dependencies": {
31
- "@quenty/baseobject": "10.14.0",
32
- "@quenty/httppromise": "10.20.0",
33
- "@quenty/jsonutils": "10.20.0",
31
+ "@quenty/baseobject": "10.15.0",
32
+ "@quenty/httppromise": "10.21.0",
33
+ "@quenty/jsonutils": "10.21.0",
34
34
  "@quenty/loader": "10.11.0",
35
- "@quenty/maid": "3.10.0",
35
+ "@quenty/maid": "3.11.0",
36
36
  "@quenty/math": "2.7.5",
37
- "@quenty/nevermore-test-runner": "1.4.0",
38
- "@quenty/promise": "10.20.0",
39
- "@quenty/rx": "13.30.0",
40
- "@quenty/servicebag": "11.19.0",
37
+ "@quenty/nevermore-test-runner": "1.5.0",
38
+ "@quenty/promise": "10.21.0",
39
+ "@quenty/rx": "13.31.0",
40
+ "@quenty/servicebag": "11.20.0",
41
41
  "@quenty/signal": "7.13.1",
42
- "@quenty/string": "3.3.7",
42
+ "@quenty/string": "3.4.0",
43
43
  "@quenty/table": "3.9.2",
44
- "@quenty/valueobject": "13.33.0",
44
+ "@quenty/valueobject": "13.34.0",
45
45
  "@quentystudios/jest-lua": "3.10.0-quenty.2"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"
49
49
  },
50
- "gitHead": "b7e59984e586064ea3cec6176e79b3f7451ecdc5"
50
+ "gitHead": "1de37218a2bedb8e3f8614a2e09bba9eddc812da"
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(clientConfig: InfluxDBClientConfigUtils.InfluxDBClientConfig?): InfluxDBClient
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
- writeAPI:SetClientConfig(clientConfig)
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,222 @@
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
+ it("should return a write API", function()
121
+ local client = InfluxDBClient.new()
122
+ local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
123
+ expect(writeAPI.ClassName).toEqual("InfluxDBWriteAPI")
124
+ client:Destroy()
125
+ end)
126
+
127
+ it("should return the same write API for the same org and bucket", function()
128
+ local client = InfluxDBClient.new()
129
+ local first = client:GetWriteAPI("my-org", "my-bucket")
130
+ local second = client:GetWriteAPI("my-org", "my-bucket")
131
+ expect(second).toBe(first)
132
+ client:Destroy()
133
+ end)
134
+
135
+ it("should return a different write API for a different bucket", function()
136
+ local client = InfluxDBClient.new()
137
+ local first = client:GetWriteAPI("my-org", "bucket-a")
138
+ local second = client:GetWriteAPI("my-org", "bucket-b")
139
+ expect(second).never.toBe(first)
140
+ client:Destroy()
141
+ end)
142
+
143
+ it("should return a different write API for a different org", function()
144
+ local client = InfluxDBClient.new()
145
+ local first = client:GetWriteAPI("org-a", "my-bucket")
146
+ local second = client:GetWriteAPI("org-b", "my-bucket")
147
+ expect(second).never.toBe(first)
148
+ client:Destroy()
149
+ end)
150
+
151
+ it("should propagate the client config set before GetWriteAPI to the write API", function()
152
+ local client, requestMock = InfluxDBClient.newMock(CONFIG)
153
+ local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
154
+
155
+ writeAPI:QueuePoint(newPoint())
156
+ expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
157
+
158
+ expect(#requestMock:GetRequests()).toEqual(1)
159
+
160
+ client:Destroy()
161
+ end)
162
+
163
+ it("should propagate a client config set after GetWriteAPI to the write API", function()
164
+ local client, requestMock = InfluxDBClient.newMock()
165
+ local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
166
+
167
+ client:SetClientConfig(CONFIG)
168
+
169
+ writeAPI:QueuePoint(newPoint())
170
+ expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
171
+
172
+ expect(#requestMock:GetRequests()).toEqual(1)
173
+
174
+ client:Destroy()
175
+ end)
176
+ end)
177
+
178
+ describe("InfluxDBClient.PromiseFlushAll", function()
179
+ it("should resolve when there are no write APIs", function()
180
+ local client = InfluxDBClient.new()
181
+
182
+ expect(PromiseTestUtils.awaitSettled(client:PromiseFlushAll())).toEqual(true)
183
+
184
+ client:Destroy()
185
+ end)
186
+
187
+ it("should flush buffered points across every write API", function()
188
+ local client, requestMock = InfluxDBClient.newMock(CONFIG)
189
+
190
+ local apiA = client:GetWriteAPI("my-org", "bucket-a")
191
+ local apiB = client:GetWriteAPI("my-org", "bucket-b")
192
+ apiA:QueuePoint(newPoint())
193
+ apiB:QueuePoint(newPoint())
194
+
195
+ expect(PromiseTestUtils.awaitSettled(client:PromiseFlushAll())).toEqual(true)
196
+ expect(#requestMock:GetRequests()).toEqual(2)
197
+
198
+ client:Destroy()
199
+ end)
200
+
201
+ it("should reuse the pending flush promise while one is in flight", function()
202
+ local client, requestMock = InfluxDBClient.newMock(CONFIG)
203
+
204
+ requestMock:SetResponder(function()
205
+ return Promise.new(function() end)
206
+ end)
207
+
208
+ local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
209
+ writeAPI:QueuePoint(newPoint())
210
+
211
+ local first = client:PromiseFlushAll()
212
+ local second = client:PromiseFlushAll()
213
+
214
+ expect(second).toBe(first)
215
+
216
+ -- Destroy cancels the in-flight request, rejecting this promise; consume it so the cancellation
217
+ -- does not surface as an unhandled rejection.
218
+ first:Catch(function() end)
219
+
220
+ client:Destroy()
221
+ end)
222
+ end)
@@ -0,0 +1,188 @@
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 InfluxDBPoint = require("InfluxDBPoint")
32
+ local InfluxDBWriteAPI = require("InfluxDBWriteAPI")
33
+ local Maid = require("Maid")
34
+ local Promise = require("Promise")
35
+ local ServiceBag = require("ServiceBag")
36
+
37
+ local InfluxDBService = {}
38
+ InfluxDBService.ServiceName = "InfluxDBService"
39
+
40
+ export type InfluxDBService = typeof(setmetatable(
41
+ {} :: {
42
+ _serviceBag: ServiceBag.ServiceBag,
43
+ _maid: Maid.Maid,
44
+ _client: InfluxDBClient.InfluxDBClient?,
45
+ _requestHandler: InfluxDBWriteAPI.InfluxDBRequestHandler?,
46
+ _pendingClientConfig: InfluxDBClientConfigUtils.InfluxDBClientConfig?,
47
+ _destroyed: boolean?,
48
+ },
49
+ {} :: typeof({ __index = InfluxDBService })
50
+ ))
51
+
52
+ --[=[
53
+ Initializes the InfluxDBService. Should be done via [ServiceBag.Init].
54
+
55
+ @param serviceBag ServiceBag
56
+ ]=]
57
+ function InfluxDBService.Init(self: InfluxDBService, serviceBag: ServiceBag.ServiceBag): ()
58
+ self._serviceBag = assert(serviceBag, "No serviceBag")
59
+ self._maid = Maid.new()
60
+ end
61
+
62
+ --[=[
63
+ Injects the request handler the underlying client uses to send data, instead of the real
64
+ [HttpPromise.request]. Pass an [InfluxDBRequestHandlerMock] handler so tests never hit the network.
65
+ Intended for testing; must be called before the client is first built.
66
+
67
+ @param requestHandler InfluxDBRequestHandler
68
+ ]=]
69
+ function InfluxDBService.SetRequestHandler(
70
+ self: InfluxDBService,
71
+ requestHandler: InfluxDBWriteAPI.InfluxDBRequestHandler
72
+ ): ()
73
+ assert(type(requestHandler) == "function", "Bad requestHandler")
74
+ assert(not self._client, "Already built client, cannot override requestHandler")
75
+
76
+ self._requestHandler = requestHandler
77
+ end
78
+
79
+ --[=[
80
+ Returns whether a request handler override was injected (see [InfluxDBService.SetRequestHandler]).
81
+ Consumers can use this to skip resolving real credentials when HTTP is intercepted.
82
+
83
+ @return boolean
84
+ ]=]
85
+ function InfluxDBService.HasRequestHandler(self: InfluxDBService): boolean
86
+ return self._requestHandler ~= nil
87
+ end
88
+
89
+ --[=[
90
+ Sets the client config used to authenticate writes. May be called before or after the client is
91
+ built; a later config is forwarded to the existing client.
92
+
93
+ @param clientConfig InfluxDBClientConfig
94
+ ]=]
95
+ function InfluxDBService.SetClientConfig(
96
+ self: InfluxDBService,
97
+ clientConfig: InfluxDBClientConfigUtils.InfluxDBClientConfig
98
+ ): ()
99
+ assert(InfluxDBClientConfigUtils.isClientConfig(clientConfig), "Bad clientConfig")
100
+
101
+ self._pendingClientConfig = clientConfig
102
+
103
+ if self._client then
104
+ self._client:SetClientConfig(clientConfig)
105
+ end
106
+ end
107
+
108
+ --[=[
109
+ Returns the shared [InfluxDBClient], building it on first use with any injected request handler and
110
+ client config.
111
+
112
+ @return InfluxDBClient
113
+ ]=]
114
+ function InfluxDBService.GetClient(self: InfluxDBService): InfluxDBClient.InfluxDBClient
115
+ if self._client then
116
+ return self._client
117
+ end
118
+
119
+ local client = self._maid:Add(InfluxDBClient.new(self._pendingClientConfig, self._requestHandler))
120
+ self._client = client
121
+
122
+ return client
123
+ end
124
+
125
+ --[=[
126
+ Returns the write API for the given org and bucket from the shared client.
127
+
128
+ @param org string
129
+ @param bucket string
130
+ @param precision string?
131
+ @return InfluxDBWriteAPI
132
+ ]=]
133
+ function InfluxDBService.GetWriteAPI(
134
+ self: InfluxDBService,
135
+ org: string,
136
+ bucket: string,
137
+ precision: string?
138
+ ): InfluxDBWriteAPI.InfluxDBWriteAPI
139
+ return self:GetClient():GetWriteAPI(org, bucket, precision)
140
+ end
141
+
142
+ --[=[
143
+ Queues a point to the shared client's write API for the given org and bucket. Consumers should
144
+ route queueing through this rather than holding the [InfluxDBWriteAPI] object: the service owns
145
+ client teardown, so it can answer liveness -- a point queued after the service is destroyed
146
+ errors, because it means the caller outlived teardown (the closing flush has already run; the
147
+ caller should have been destroyed first).
148
+
149
+ @param org string
150
+ @param bucket string
151
+ @param point InfluxDBPoint
152
+ ]=]
153
+ function InfluxDBService.QueuePoint(self: InfluxDBService, org: string, bucket: string, point): ()
154
+ assert(type(org) == "string", "Bad org")
155
+ assert(type(bucket) == "string", "Bad bucket")
156
+ assert(InfluxDBPoint.isInfluxDBPoint(point), "Bad point")
157
+
158
+ if self._destroyed then
159
+ error(
160
+ string.format(
161
+ "[InfluxDBService.QueuePoint] - Queued %q after InfluxDBService was destroyed; the caller outlived teardown",
162
+ tostring(point:GetMeasurementName())
163
+ )
164
+ )
165
+ end
166
+
167
+ self:GetWriteAPI(org, bucket):QueuePoint(point)
168
+ end
169
+
170
+ --[=[
171
+ Flushes every write API on the shared client. Resolves immediately if the client was never built.
172
+
173
+ @return Promise<()>
174
+ ]=]
175
+ function InfluxDBService.PromiseFlushAll(self: InfluxDBService): Promise.Promise<()>
176
+ if not self._client then
177
+ return Promise.resolved()
178
+ end
179
+
180
+ return self._client:PromiseFlushAll()
181
+ end
182
+
183
+ function InfluxDBService.Destroy(self: InfluxDBService): ()
184
+ self._destroyed = true
185
+ self._maid:DoCleaning()
186
+ end
187
+
188
+ return InfluxDBService
@@ -0,0 +1,147 @@
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
+ local function setup()
34
+ local serviceBag = ServiceBag.new()
35
+ local influxDBService: InfluxDBService.InfluxDBService = serviceBag:GetService(InfluxDBService) :: any
36
+ serviceBag:Init()
37
+
38
+ local requestMock = InfluxDBRequestHandlerMock.new()
39
+ influxDBService:SetRequestHandler(requestMock.Handler)
40
+
41
+ serviceBag:Start()
42
+
43
+ return serviceBag, influxDBService, requestMock
44
+ end
45
+
46
+ describe("InfluxDBService.SetRequestHandler", function()
47
+ it("should route write traffic through the injected mock", function()
48
+ local serviceBag, influxDBService, requestMock = setup()
49
+ influxDBService:SetClientConfig(CONFIG)
50
+
51
+ local writeAPI = influxDBService:GetWriteAPI("my-org", "my-bucket")
52
+ writeAPI:QueuePoint(newPoint())
53
+ expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
54
+
55
+ expect(#requestMock:GetRequests()).toEqual(1)
56
+ expect((requestMock:GetLastRequest() :: any).Headers.Authorization).toEqual("Token my-token")
57
+
58
+ serviceBag:Destroy()
59
+ end)
60
+
61
+ it("should throw on a non-function handler", function()
62
+ local serviceBag = ServiceBag.new()
63
+ local influxDBService: InfluxDBService.InfluxDBService = serviceBag:GetService(InfluxDBService) :: any
64
+ serviceBag:Init()
65
+
66
+ expect(function()
67
+ influxDBService:SetRequestHandler(5 :: any)
68
+ end).toThrow("Bad requestHandler")
69
+
70
+ serviceBag:Destroy()
71
+ end)
72
+
73
+ it("should throw once the client has been built", function()
74
+ local serviceBag = ServiceBag.new()
75
+ local influxDBService: InfluxDBService.InfluxDBService = serviceBag:GetService(InfluxDBService) :: any
76
+ serviceBag:Init()
77
+ serviceBag:Start()
78
+
79
+ influxDBService:GetClient()
80
+
81
+ expect(function()
82
+ influxDBService:SetRequestHandler(InfluxDBRequestHandlerMock.new().Handler)
83
+ end).toThrow("Already built client")
84
+
85
+ serviceBag:Destroy()
86
+ end)
87
+ end)
88
+
89
+ describe("InfluxDBService.SetClientConfig", function()
90
+ it("should throw on an invalid config", function()
91
+ local serviceBag, influxDBService = setup()
92
+
93
+ expect(function()
94
+ influxDBService:SetClientConfig({ url = "https://example.com" } :: any)
95
+ end).toThrow("Bad clientConfig")
96
+
97
+ serviceBag:Destroy()
98
+ end)
99
+
100
+ it("should apply a config set after the client is built", function()
101
+ local serviceBag, influxDBService, requestMock = setup()
102
+
103
+ local writeAPI = influxDBService:GetWriteAPI("my-org", "my-bucket")
104
+
105
+ influxDBService:SetClientConfig(CONFIG)
106
+
107
+ writeAPI:QueuePoint(newPoint())
108
+ expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
109
+
110
+ expect(#requestMock:GetRequests()).toEqual(1)
111
+
112
+ serviceBag:Destroy()
113
+ end)
114
+ end)
115
+
116
+ describe("InfluxDBService.GetClient", function()
117
+ it("should return the same client across calls", function()
118
+ local serviceBag, influxDBService = setup()
119
+
120
+ expect(influxDBService:GetClient()).toBe(influxDBService:GetClient())
121
+
122
+ serviceBag:Destroy()
123
+ end)
124
+ end)
125
+
126
+ describe("InfluxDBService.PromiseFlushAll", function()
127
+ it("should resolve when the client was never built", function()
128
+ local serviceBag, influxDBService = setup()
129
+
130
+ expect(PromiseTestUtils.awaitSettled(influxDBService:PromiseFlushAll())).toEqual(true)
131
+
132
+ serviceBag:Destroy()
133
+ end)
134
+
135
+ it("should flush buffered points once the client is built", function()
136
+ local serviceBag, influxDBService, requestMock = setup()
137
+ influxDBService:SetClientConfig(CONFIG)
138
+
139
+ local writeAPI = influxDBService:GetWriteAPI("my-org", "my-bucket")
140
+ writeAPI:QueuePoint(newPoint())
141
+
142
+ expect(PromiseTestUtils.awaitSettled(influxDBService:PromiseFlushAll())).toEqual(true)
143
+ expect(#requestMock:GetRequests()).toEqual(1)
144
+
145
+ serviceBag:Destroy()
146
+ end)
147
+ 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(org: string, bucket: string, precision: string?): InfluxDBWriteAPI
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(HttpPromise.request(request))
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)