matterbridge-lg-thinq-plugin 0.1.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/.claude/CLAUDE.md +43 -0
- package/.claude/agents/compiler.md +91 -0
- package/.claude/agents/direct-executor.md +98 -0
- package/.claude/agents/documenter.md +78 -0
- package/.claude/agents/finalizer.md +233 -0
- package/.claude/agents/implementer.md +112 -0
- package/.claude/agents/investigator.md +100 -0
- package/.claude/agents/release-manager.md +203 -0
- package/.claude/agents/reviewer.md +131 -0
- package/.claude/agents/technical-architect.md +403 -0
- package/.claude/agents/test-writer.md +159 -0
- package/.claude/agents/wiki-manager.md +147 -0
- package/.claude/instructions/agent-prompts.md +90 -0
- package/.claude/instructions/shared-rules.md +11 -0
- package/.claude/instructions/team-orchestrator-policy.md +165 -0
- package/.claude/memory.md +186 -0
- package/.claude/output-styles/engineer-manager.md +38 -0
- package/.claude/settings.json +40 -0
- package/.claude/settings.local.json +30 -0
- package/.claude/skills/load-policy/SKILL.md +13 -0
- package/.claude/skills/ref-idea/SKILL.md +44 -0
- package/.claude/skills/status-of/SKILL.md +38 -0
- package/.claude/skills/workspace-console/SKILL.md +49 -0
- package/.claude/statusline-command.sh +130 -0
- package/.claude/statusline.md +53 -0
- package/.claude/templates/approval-packet.template.html +171 -0
- package/.claude/templates/reference-workspaces.md +16 -0
- package/.claude/tools/cli-runner-server.cjs +220 -0
- package/.claude/tools/discord-send-server.cjs +188 -0
- package/.claude/tools/github-release-server.cjs +181 -0
- package/.claude/tools/glob-grep-server.cjs +267 -0
- package/.claude/tools/read-log-server.cjs +175 -0
- package/.github/codeql/codeql-config.yml +14 -0
- package/.github/workflows/build.yml +54 -0
- package/.github/workflows/codeql.yml +59 -0
- package/.github/workflows/coverage.yml +70 -0
- package/.github/workflows/publish.yml +103 -0
- package/.mcp.json +34 -0
- package/CHANGELOG.md +195 -0
- package/LICENSE +21 -0
- package/dist/cli/commands/devices.js +52 -0
- package/dist/cli/commands/energy.js +122 -0
- package/dist/cli/commands/login.js +54 -0
- package/dist/cli/help.js +50 -0
- package/dist/cli/main.js +56 -0
- package/dist/cli/session.js +13 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli/utils.js +29 -0
- package/dist/cli.js +2 -0
- package/dist/constants/index.js +1 -0
- package/dist/core/domain/entities/ThinqDevice.js +43 -0
- package/dist/core/domain/value-objects/AirConditionerCapabilities.js +22 -0
- package/dist/core/domain/value-objects/ThinqFilterState.js +35 -0
- package/dist/core/domain/value-objects/ThinqSnapshot.js +125 -0
- package/dist/errors/AuthenticationError.js +26 -0
- package/dist/errors/BaseError.js +23 -0
- package/dist/errors/index.js +2 -0
- package/dist/model/LgThinkqPluginPlatformConfig.js +23 -0
- package/dist/model/LgThinqPluginPlatformConfig.js +23 -0
- package/dist/module.js +212 -0
- package/dist/platform/deviceRegistry.js +26 -0
- package/dist/platform/platformConfigManager.js +89 -0
- package/dist/platform/platformState.js +9 -0
- package/dist/platform/thinq/thinqAirConditionerCommandHandlers.js +190 -0
- package/dist/platform/thinq/thinqAirConditionerEndpointFactory.js +53 -0
- package/dist/platform/thinq/thinqAirConditionerFilterResetCommandHandler.js +54 -0
- package/dist/platform/thinq/thinqAirConditionerFilterStateSync.js +19 -0
- package/dist/platform/thinq/thinqAirConditionerStateSync.js +205 -0
- package/dist/platform/thinq/thinqDeviceConfigReconciler.js +26 -0
- package/dist/platform/thinq/thinqDeviceConfigurator.js +124 -0
- package/dist/platform/thinq/thinqDeviceDiscovery.js +33 -0
- package/dist/platform/thinq/thinqWasherCommandHandlers.js +54 -0
- package/dist/platform/thinq/thinqWasherEndpointFactory.js +6 -0
- package/dist/platform/thinq/thinqWasherStateSync.js +31 -0
- package/dist/platform/thinq/thinqWasherStopCommandResolver.js +38 -0
- package/dist/services/authentication/AccountAuthStrategy.js +135 -0
- package/dist/services/authentication/AuthContext.js +1 -0
- package/dist/services/authentication/AuthenticationCoordinator.js +29 -0
- package/dist/services/authentication/IAuthStrategy.js +1 -0
- package/dist/services/authentication/TokenAuthStrategy.js +27 -0
- package/dist/services/authentication/UserDataRepository.js +32 -0
- package/dist/services/thinq/gateway.js +61 -0
- package/dist/services/thinq/mqtt/mqttCertificate.js +63 -0
- package/dist/services/thinq/mqtt/mqttConnectionEvents.js +37 -0
- package/dist/services/thinq/mqtt/mqttKeyRepository.js +33 -0
- package/dist/services/thinq/mqtt/mqttRetry.js +25 -0
- package/dist/services/thinq/mqtt/thinqMqttListener.js +97 -0
- package/dist/services/thinq/serviceContainer.js +73 -0
- package/dist/services/thinq/session.js +48 -0
- package/dist/services/thinq/thinqApiClient.js +258 -0
- package/dist/services/thinq/thinqDeviceService.js +75 -0
- package/dist/services/thinq/thinqFilterMonitoringService.js +40 -0
- package/dist/settings.js +9 -0
- package/dist/tests/helpers/testUtils.js +87 -0
- package/dist/utils/mask.js +39 -0
- package/docs/README_CLI.md +166 -0
- package/eslint.config.js +194 -0
- package/matterbridge-lg-thinq-plugin.config.json +27 -0
- package/matterbridge-lg-thinq-plugin.schema.json +520 -0
- package/package.json +107 -0
- package/prettier.config.js +49 -0
- package/scripts/parse-junit-failures.mjs +45 -0
- package/scripts/parse-tsc-failures.mjs +48 -0
- package/scripts/run-build-local-summary.mjs +29 -0
- package/scripts/run-format-summary.mjs +36 -0
- package/scripts/run-lint-fix-summary.mjs +38 -0
- package/scripts/run-precommit-summary.mjs +94 -0
- package/src/cli/commands/devices.ts +67 -0
- package/src/cli/commands/energy.ts +160 -0
- package/src/cli/commands/login.ts +74 -0
- package/src/cli/help.ts +53 -0
- package/src/cli/main.ts +66 -0
- package/src/cli/session.ts +15 -0
- package/src/cli/types.ts +10 -0
- package/src/cli/utils.ts +30 -0
- package/src/cli.ts +3 -0
- package/src/constants/index.ts +8 -0
- package/src/core/domain/entities/ThinqDevice.test.ts +224 -0
- package/src/core/domain/entities/ThinqDevice.ts +74 -0
- package/src/core/domain/value-objects/AirConditionerCapabilities.ts +50 -0
- package/src/core/domain/value-objects/ThinqFilterState.ts +50 -0
- package/src/core/domain/value-objects/ThinqSnapshot.ts +162 -0
- package/src/errors/AuthenticationError.ts +37 -0
- package/src/errors/BaseError.ts +38 -0
- package/src/errors/index.ts +24 -0
- package/src/model/LgThinqPluginPlatformConfig.ts +94 -0
- package/src/module.ts +298 -0
- package/src/platform/deviceRegistry.ts +58 -0
- package/src/platform/platformConfigManager.ts +133 -0
- package/src/platform/platformState.ts +16 -0
- package/src/platform/thinq/thinqAirConditionerCommandHandlers.ts +291 -0
- package/src/platform/thinq/thinqAirConditionerEndpointFactory.ts +145 -0
- package/src/platform/thinq/thinqAirConditionerFilterResetCommandHandler.ts +96 -0
- package/src/platform/thinq/thinqAirConditionerFilterStateSync.ts +42 -0
- package/src/platform/thinq/thinqAirConditionerStateSync.ts +343 -0
- package/src/platform/thinq/thinqDeviceConfigReconciler.ts +42 -0
- package/src/platform/thinq/thinqDeviceConfigurator.test.ts +90 -0
- package/src/platform/thinq/thinqDeviceConfigurator.ts +191 -0
- package/src/platform/thinq/thinqDeviceDiscovery.test.ts +173 -0
- package/src/platform/thinq/thinqDeviceDiscovery.ts +51 -0
- package/src/platform/thinq/thinqWasherCommandHandlers.ts +120 -0
- package/src/platform/thinq/thinqWasherEndpointFactory.ts +37 -0
- package/src/platform/thinq/thinqWasherStateSync.ts +60 -0
- package/src/platform/thinq/thinqWasherStopCommandResolver.ts +62 -0
- package/src/services/authentication/AccountAuthStrategy.test.ts +358 -0
- package/src/services/authentication/AccountAuthStrategy.ts +209 -0
- package/src/services/authentication/AuthContext.ts +19 -0
- package/src/services/authentication/AuthenticationCoordinator.test.ts +217 -0
- package/src/services/authentication/AuthenticationCoordinator.ts +59 -0
- package/src/services/authentication/IAuthStrategy.ts +10 -0
- package/src/services/authentication/TokenAuthStrategy.test.ts +167 -0
- package/src/services/authentication/TokenAuthStrategy.ts +40 -0
- package/src/services/authentication/UserDataRepository.test.ts +149 -0
- package/src/services/authentication/UserDataRepository.ts +46 -0
- package/src/services/thinq/gateway.ts +86 -0
- package/src/services/thinq/mqtt/mqttCertificate.test.ts +344 -0
- package/src/services/thinq/mqtt/mqttCertificate.ts +102 -0
- package/src/services/thinq/mqtt/mqttConnectionEvents.test.ts +276 -0
- package/src/services/thinq/mqtt/mqttConnectionEvents.ts +76 -0
- package/src/services/thinq/mqtt/mqttKeyRepository.test.ts +164 -0
- package/src/services/thinq/mqtt/mqttKeyRepository.ts +43 -0
- package/src/services/thinq/mqtt/mqttRetry.test.ts +239 -0
- package/src/services/thinq/mqtt/mqttRetry.ts +43 -0
- package/src/services/thinq/mqtt/thinqMqttListener.test.ts +349 -0
- package/src/services/thinq/mqtt/thinqMqttListener.ts +120 -0
- package/src/services/thinq/serviceContainer.test.ts +203 -0
- package/src/services/thinq/serviceContainer.ts +105 -0
- package/src/services/thinq/session.test.ts +287 -0
- package/src/services/thinq/session.ts +69 -0
- package/src/services/thinq/thinqApiClient.test.ts +702 -0
- package/src/services/thinq/thinqApiClient.ts +418 -0
- package/src/services/thinq/thinqDeviceService.test.ts +415 -0
- package/src/services/thinq/thinqDeviceService.ts +101 -0
- package/src/services/thinq/thinqFilterMonitoringService.test.ts +264 -0
- package/src/services/thinq/thinqFilterMonitoringService.ts +53 -0
- package/src/settings.ts +18 -0
- package/src/tests/cli/commands/devices.test.ts +293 -0
- package/src/tests/cli/commands/login.test.ts +240 -0
- package/src/tests/cli/help.test.ts +46 -0
- package/src/tests/cli/main.test.ts +214 -0
- package/src/tests/cli/session.test.ts +198 -0
- package/src/tests/cli/utils.test.ts +194 -0
- package/src/tests/core/domain/value-objects/AirConditionerCapabilities.test.ts +685 -0
- package/src/tests/core/domain/value-objects/ThinqFilterState.test.ts +210 -0
- package/src/tests/core/domain/value-objects/ThinqSnapshot.test.ts +1035 -0
- package/src/tests/helpers/testUtils.ts +118 -0
- package/src/tests/model/LgThinqPluginPlatformConfig.test.ts +81 -0
- package/src/tests/platform/deviceRegistry.test.ts +177 -0
- package/src/tests/platform/platformConfigManager.test.ts +790 -0
- package/src/tests/platform/platformState.test.ts +41 -0
- package/src/tests/platform/thinq/thinqAirConditionerCommandHandlers.test.ts +879 -0
- package/src/tests/platform/thinq/thinqAirConditionerEndpointFactory.test.ts +993 -0
- package/src/tests/platform/thinq/thinqAirConditionerFilterResetCommandHandler.test.ts +508 -0
- package/src/tests/platform/thinq/thinqAirConditionerFilterStateSync.test.ts +212 -0
- package/src/tests/platform/thinq/thinqAirConditionerStateSync.test.ts +1586 -0
- package/src/tests/platform/thinq/thinqDeviceConfigReconciler.test.ts +269 -0
- package/src/tests/platform/thinq/thinqDeviceConfigurator.test.ts +492 -0
- package/src/tests/platform/thinq/thinqWasherCommandHandlers.test.ts +594 -0
- package/src/tests/platform/thinq/thinqWasherEndpointFactory.test.ts +84 -0
- package/src/tests/platform/thinq/thinqWasherStateSync.test.ts +373 -0
- package/src/tests/platform/thinq/thinqWasherStopCommandResolver.test.ts +392 -0
- package/src/tests/settings.test.ts +33 -0
- package/src/utils/mask.ts +50 -0
- package/tsconfig.base.json +25 -0
- package/tsconfig.json +9 -0
- package/tsconfig.production.json +13 -0
- package/tsconfig.vitest.json +5 -0
- package/vite.config.ts +43 -0
- package/wiki/memory-archive.md +12 -0
- package/wiki/reference-workspaces.md +23 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { ThinqSession } from './session.js';
|
|
4
|
+
|
|
5
|
+
describe('ThinqSession', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('constructor and getters', () => {
|
|
11
|
+
it('should set accessToken getter to provided value', () => {
|
|
12
|
+
const session = new ThinqSession('access123', 'refresh456', 1600000000);
|
|
13
|
+
|
|
14
|
+
expect(session.accessToken).toBe('access123');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should set refreshToken getter to provided value', () => {
|
|
18
|
+
const session = new ThinqSession('access123', 'refresh456', 1600000000);
|
|
19
|
+
|
|
20
|
+
expect(session.refreshToken).toBe('refresh456');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should set expiresAtEpochSeconds getter to provided value', () => {
|
|
24
|
+
const session = new ThinqSession('access123', 'refresh456', 1600000000);
|
|
25
|
+
|
|
26
|
+
expect(session.expiresAtEpochSeconds).toBe(1600000000);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('hasToken', () => {
|
|
31
|
+
it('should return true for non-empty accessToken', () => {
|
|
32
|
+
const session = new ThinqSession('access123', 'refresh456', 1600000000);
|
|
33
|
+
|
|
34
|
+
expect(session.hasToken()).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return false for empty accessToken', () => {
|
|
38
|
+
const session = new ThinqSession('', 'refresh456', 1600000000);
|
|
39
|
+
|
|
40
|
+
expect(session.hasToken()).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('isExpired', () => {
|
|
45
|
+
it('should return true when expiresAtEpochSeconds is in the past', () => {
|
|
46
|
+
vi.useFakeTimers();
|
|
47
|
+
const now = new Date('2025-01-01T00:00:00Z').getTime();
|
|
48
|
+
vi.setSystemTime(now);
|
|
49
|
+
|
|
50
|
+
// Set expiry to 1 hour ago
|
|
51
|
+
const expiredTime = Math.round((now - 3600000) / 1000);
|
|
52
|
+
const session = new ThinqSession('access123', 'refresh456', expiredTime);
|
|
53
|
+
|
|
54
|
+
expect(session.isExpired()).toBe(true);
|
|
55
|
+
|
|
56
|
+
vi.useRealTimers();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return false when expiresAtEpochSeconds is in the future', () => {
|
|
60
|
+
vi.useFakeTimers();
|
|
61
|
+
const now = new Date('2025-01-01T00:00:00Z').getTime();
|
|
62
|
+
vi.setSystemTime(now);
|
|
63
|
+
|
|
64
|
+
// Set expiry to 1 hour in the future
|
|
65
|
+
const futureTime = Math.round((now + 3600000) / 1000);
|
|
66
|
+
const session = new ThinqSession('access123', 'refresh456', futureTime);
|
|
67
|
+
|
|
68
|
+
expect(session.isExpired()).toBe(false);
|
|
69
|
+
|
|
70
|
+
vi.useRealTimers();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return false when expiresAtEpochSeconds equals current time', () => {
|
|
74
|
+
vi.useFakeTimers();
|
|
75
|
+
const now = new Date('2025-01-01T00:00:00Z').getTime();
|
|
76
|
+
vi.setSystemTime(now);
|
|
77
|
+
|
|
78
|
+
// Set expiry to exactly now (boundary condition: equal means not expired)
|
|
79
|
+
const currentTime = Math.round(now / 1000);
|
|
80
|
+
const session = new ThinqSession('access123', 'refresh456', currentTime);
|
|
81
|
+
|
|
82
|
+
expect(session.isExpired()).toBe(false);
|
|
83
|
+
|
|
84
|
+
vi.useRealTimers();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('hasValidToken', () => {
|
|
89
|
+
it('should return true only when token exists and is not expired', () => {
|
|
90
|
+
vi.useFakeTimers();
|
|
91
|
+
const now = new Date('2025-01-01T00:00:00Z').getTime();
|
|
92
|
+
vi.setSystemTime(now);
|
|
93
|
+
|
|
94
|
+
// Valid: has token and not expired
|
|
95
|
+
const futureTime = Math.round((now + 3600000) / 1000);
|
|
96
|
+
const session = new ThinqSession('access123', 'refresh456', futureTime);
|
|
97
|
+
|
|
98
|
+
expect(session.hasValidToken()).toBe(true);
|
|
99
|
+
|
|
100
|
+
vi.useRealTimers();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return false when token is empty but not expired', () => {
|
|
104
|
+
vi.useFakeTimers();
|
|
105
|
+
const now = new Date('2025-01-01T00:00:00Z').getTime();
|
|
106
|
+
vi.setSystemTime(now);
|
|
107
|
+
|
|
108
|
+
// No token: empty accessToken and not expired
|
|
109
|
+
const futureTime = Math.round((now + 3600000) / 1000);
|
|
110
|
+
const session = new ThinqSession('', 'refresh456', futureTime);
|
|
111
|
+
|
|
112
|
+
expect(session.hasValidToken()).toBe(false);
|
|
113
|
+
|
|
114
|
+
vi.useRealTimers();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return false when token exists but is expired', () => {
|
|
118
|
+
vi.useFakeTimers();
|
|
119
|
+
const now = new Date('2025-01-01T00:00:00Z').getTime();
|
|
120
|
+
vi.setSystemTime(now);
|
|
121
|
+
|
|
122
|
+
// Expired: has token but is expired
|
|
123
|
+
const pastTime = Math.round((now - 3600000) / 1000);
|
|
124
|
+
const session = new ThinqSession('access123', 'refresh456', pastTime);
|
|
125
|
+
|
|
126
|
+
expect(session.hasValidToken()).toBe(false);
|
|
127
|
+
|
|
128
|
+
vi.useRealTimers();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should return false when token is empty and expired', () => {
|
|
132
|
+
vi.useFakeTimers();
|
|
133
|
+
const now = new Date('2025-01-01T00:00:00Z').getTime();
|
|
134
|
+
vi.setSystemTime(now);
|
|
135
|
+
|
|
136
|
+
// Both invalid: empty accessToken and expired
|
|
137
|
+
const pastTime = Math.round((now - 3600000) / 1000);
|
|
138
|
+
const session = new ThinqSession('', 'refresh456', pastTime);
|
|
139
|
+
|
|
140
|
+
expect(session.hasValidToken()).toBe(false);
|
|
141
|
+
|
|
142
|
+
vi.useRealTimers();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('updateAccessToken', () => {
|
|
147
|
+
it('should update accessToken and expiresAtEpochSeconds in place', () => {
|
|
148
|
+
const session = new ThinqSession('old-access', 'refresh456', 1600000000);
|
|
149
|
+
const originalRefreshToken = session.refreshToken;
|
|
150
|
+
|
|
151
|
+
session.updateAccessToken('new-access', 1700000000);
|
|
152
|
+
|
|
153
|
+
expect(session.accessToken).toBe('new-access');
|
|
154
|
+
expect(session.expiresAtEpochSeconds).toBe(1700000000);
|
|
155
|
+
expect(session.refreshToken).toBe(originalRefreshToken);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should not change refreshToken when updating accessToken', () => {
|
|
159
|
+
const session = new ThinqSession('old-access', 'original-refresh', 1600000000);
|
|
160
|
+
|
|
161
|
+
session.updateAccessToken('new-access', 1700000000);
|
|
162
|
+
|
|
163
|
+
expect(session.refreshToken).toBe('original-refresh');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should allow multiple updates', () => {
|
|
167
|
+
const session = new ThinqSession('access1', 'refresh456', 1600000000);
|
|
168
|
+
|
|
169
|
+
session.updateAccessToken('access2', 1650000000);
|
|
170
|
+
expect(session.accessToken).toBe('access2');
|
|
171
|
+
expect(session.expiresAtEpochSeconds).toBe(1650000000);
|
|
172
|
+
|
|
173
|
+
session.updateAccessToken('access3', 1700000000);
|
|
174
|
+
expect(session.accessToken).toBe('access3');
|
|
175
|
+
expect(session.expiresAtEpochSeconds).toBe(1700000000);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('expiryFromExpiresIn', () => {
|
|
180
|
+
it('should return current epoch seconds plus expiresIn seconds', () => {
|
|
181
|
+
vi.useFakeTimers();
|
|
182
|
+
const now = new Date('2025-01-01T00:00:00Z').getTime();
|
|
183
|
+
vi.setSystemTime(now);
|
|
184
|
+
|
|
185
|
+
const currentEpochSeconds = Math.round(now / 1000);
|
|
186
|
+
const expiresIn = 3600; // 1 hour
|
|
187
|
+
|
|
188
|
+
const result = ThinqSession.expiryFromExpiresIn(expiresIn);
|
|
189
|
+
|
|
190
|
+
expect(result).toBe(currentEpochSeconds + expiresIn);
|
|
191
|
+
|
|
192
|
+
vi.useRealTimers();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle zero expiresIn', () => {
|
|
196
|
+
vi.useFakeTimers();
|
|
197
|
+
const now = new Date('2025-01-01T00:00:00Z').getTime();
|
|
198
|
+
vi.setSystemTime(now);
|
|
199
|
+
|
|
200
|
+
const currentEpochSeconds = Math.round(now / 1000);
|
|
201
|
+
|
|
202
|
+
const result = ThinqSession.expiryFromExpiresIn(0);
|
|
203
|
+
|
|
204
|
+
expect(result).toBe(currentEpochSeconds);
|
|
205
|
+
|
|
206
|
+
vi.useRealTimers();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should handle negative expiresIn', () => {
|
|
210
|
+
vi.useFakeTimers();
|
|
211
|
+
const now = new Date('2025-01-01T00:00:00Z').getTime();
|
|
212
|
+
vi.setSystemTime(now);
|
|
213
|
+
|
|
214
|
+
const currentEpochSeconds = Math.round(now / 1000);
|
|
215
|
+
const expiresIn = -3600; // 1 hour in the past
|
|
216
|
+
|
|
217
|
+
const result = ThinqSession.expiryFromExpiresIn(expiresIn);
|
|
218
|
+
|
|
219
|
+
expect(result).toBe(currentEpochSeconds + expiresIn);
|
|
220
|
+
|
|
221
|
+
vi.useRealTimers();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('toData', () => {
|
|
226
|
+
it('should return an object with all three fields', () => {
|
|
227
|
+
const session = new ThinqSession('access123', 'refresh456', 1600000000);
|
|
228
|
+
|
|
229
|
+
const data = session.toData();
|
|
230
|
+
|
|
231
|
+
expect(data).toEqual({
|
|
232
|
+
accessToken: 'access123',
|
|
233
|
+
refreshToken: 'refresh456',
|
|
234
|
+
expiresAtEpochSeconds: 1600000000,
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should return current field values even after update', () => {
|
|
239
|
+
const session = new ThinqSession('old-access', 'refresh456', 1600000000);
|
|
240
|
+
session.updateAccessToken('new-access', 1700000000);
|
|
241
|
+
|
|
242
|
+
const data = session.toData();
|
|
243
|
+
|
|
244
|
+
expect(data).toEqual({
|
|
245
|
+
accessToken: 'new-access',
|
|
246
|
+
refreshToken: 'refresh456',
|
|
247
|
+
expiresAtEpochSeconds: 1700000000,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('fromData', () => {
|
|
253
|
+
it('should construct a new ThinqSession from ThinqSessionData', () => {
|
|
254
|
+
const data = {
|
|
255
|
+
accessToken: 'access123',
|
|
256
|
+
refreshToken: 'refresh456',
|
|
257
|
+
expiresAtEpochSeconds: 1600000000,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const session = ThinqSession.fromData(data);
|
|
261
|
+
|
|
262
|
+
expect(session.accessToken).toBe('access123');
|
|
263
|
+
expect(session.refreshToken).toBe('refresh456');
|
|
264
|
+
expect(session.expiresAtEpochSeconds).toBe(1600000000);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should round-trip correctly: toData -> fromData produces equivalent session', () => {
|
|
268
|
+
const original = new ThinqSession('access123', 'refresh456', 1600000000);
|
|
269
|
+
const data = original.toData();
|
|
270
|
+
const restored = ThinqSession.fromData(data);
|
|
271
|
+
|
|
272
|
+
expect(restored.accessToken).toBe(original.accessToken);
|
|
273
|
+
expect(restored.refreshToken).toBe(original.refreshToken);
|
|
274
|
+
expect(restored.expiresAtEpochSeconds).toBe(original.expiresAtEpochSeconds);
|
|
275
|
+
expect(restored.hasToken()).toBe(original.hasToken());
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should handle empty accessToken in round-trip', () => {
|
|
279
|
+
const original = new ThinqSession('', 'refresh456', 1600000000);
|
|
280
|
+
const data = original.toData();
|
|
281
|
+
const restored = ThinqSession.fromData(data);
|
|
282
|
+
|
|
283
|
+
expect(restored.hasToken()).toBe(false);
|
|
284
|
+
expect(restored.refreshToken).toBe('refresh456');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Persistable shape of a `ThinqSession`. */
|
|
2
|
+
export interface ThinqSessionData {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken: string;
|
|
5
|
+
expiresAtEpochSeconds: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Holds a ThinQ OAuth access/refresh token pair and its expiry, mirroring `homebridge-lg-thinq`'s `Session`. */
|
|
9
|
+
export class ThinqSession {
|
|
10
|
+
private accessTokenValue: string;
|
|
11
|
+
private readonly refreshTokenValue: string;
|
|
12
|
+
private expiresAtEpochSecondsValue: number;
|
|
13
|
+
|
|
14
|
+
constructor(accessToken: string, refreshToken: string, expiresAtEpochSeconds: number) {
|
|
15
|
+
this.accessTokenValue = accessToken;
|
|
16
|
+
this.refreshTokenValue = refreshToken;
|
|
17
|
+
this.expiresAtEpochSecondsValue = expiresAtEpochSeconds;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public get accessToken(): string {
|
|
21
|
+
return this.accessTokenValue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public get refreshToken(): string {
|
|
25
|
+
return this.refreshTokenValue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public get expiresAtEpochSeconds(): number {
|
|
29
|
+
return this.expiresAtEpochSecondsValue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public hasToken(): boolean {
|
|
33
|
+
return this.accessTokenValue !== '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public isExpired(): boolean {
|
|
37
|
+
return this.expiresAtEpochSecondsValue < ThinqSession.currentEpochSeconds();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public hasValidToken(): boolean {
|
|
41
|
+
return this.hasToken() && !this.isExpired();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Mutates this session in place with a freshly issued access token (refresh token is never rotated). */
|
|
45
|
+
public updateAccessToken(accessToken: string, expiresAtEpochSeconds: number): void {
|
|
46
|
+
this.accessTokenValue = accessToken;
|
|
47
|
+
this.expiresAtEpochSecondsValue = expiresAtEpochSeconds;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public static expiryFromExpiresIn(expiresInSeconds: number): number {
|
|
51
|
+
return ThinqSession.currentEpochSeconds() + expiresInSeconds;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public toData(): ThinqSessionData {
|
|
55
|
+
return {
|
|
56
|
+
accessToken: this.accessTokenValue,
|
|
57
|
+
refreshToken: this.refreshTokenValue,
|
|
58
|
+
expiresAtEpochSeconds: this.expiresAtEpochSecondsValue,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public static fromData(data: ThinqSessionData): ThinqSession {
|
|
63
|
+
return new ThinqSession(data.accessToken, data.refreshToken, data.expiresAtEpochSeconds);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static currentEpochSeconds(): number {
|
|
67
|
+
return Math.round(Date.now() / 1000);
|
|
68
|
+
}
|
|
69
|
+
}
|