|
1 | 1 | // Env must be set BEFORE importing the module under test so its constants |
2 | | -// resolve to the test values. |
| 2 | +// resolve to the test values. KILO_CHAT_INTERNAL_URL is the preferred |
| 3 | +// server-only destination; NEXT_PUBLIC_KILO_CHAT_URL is the migration-safe |
| 4 | +// fallback. |
| 5 | +process.env.KILO_CHAT_INTERNAL_URL = 'https://chat.kiloapps.io'; |
3 | 6 | process.env.NEXT_PUBLIC_KILO_CHAT_URL = 'https://chat.kiloapps.io'; |
4 | 7 |
|
5 | 8 | jest.mock('@/lib/config.server', () => ({ |
@@ -113,29 +116,81 @@ describe('postMessageAsUser (cloud → kilo-chat internal HTTP)', () => { |
113 | 116 | await expect(postMessageAsUser(VALID_PARAMS)).rejects.toThrow(/unexpected payload/); |
114 | 117 | }); |
115 | 118 |
|
116 | | - it('throws when NEXT_PUBLIC_KILO_CHAT_URL is missing', async () => { |
117 | | - const saved = process.env.NEXT_PUBLIC_KILO_CHAT_URL; |
| 119 | + it('prefers KILO_CHAT_INTERNAL_URL over NEXT_PUBLIC_KILO_CHAT_URL', async () => { |
| 120 | + const savedInternal = process.env.KILO_CHAT_INTERNAL_URL; |
| 121 | + const savedPublic = process.env.NEXT_PUBLIC_KILO_CHAT_URL; |
| 122 | + // Point the public var somewhere off-allowlist: if it were used, the call |
| 123 | + // would be refused. The internal var must win and the fetch must go to it. |
| 124 | + process.env.KILO_CHAT_INTERNAL_URL = 'https://chat.kiloapps.io'; |
| 125 | + process.env.NEXT_PUBLIC_KILO_CHAT_URL = 'https://evil.example.com'; |
| 126 | + const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue( |
| 127 | + jsonResponse({ |
| 128 | + ok: true, |
| 129 | + conversationId: 'conv-1', |
| 130 | + messageId: 'msg-1', |
| 131 | + conversationCreated: false, |
| 132 | + }) |
| 133 | + ); |
| 134 | + try { |
| 135 | + await postMessageAsUser(VALID_PARAMS); |
| 136 | + const [url] = fetchSpy.mock.calls[0]!; |
| 137 | + expect(url).toBe('https://chat.kiloapps.io/internal/v1/post-message-as-user'); |
| 138 | + } finally { |
| 139 | + process.env.KILO_CHAT_INTERNAL_URL = savedInternal; |
| 140 | + process.env.NEXT_PUBLIC_KILO_CHAT_URL = savedPublic; |
| 141 | + } |
| 142 | + }); |
| 143 | + |
| 144 | + it('falls back to NEXT_PUBLIC_KILO_CHAT_URL with a warning when the internal var is unset', async () => { |
| 145 | + const savedInternal = process.env.KILO_CHAT_INTERNAL_URL; |
| 146 | + delete process.env.KILO_CHAT_INTERNAL_URL; |
| 147 | + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); |
| 148 | + const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue( |
| 149 | + jsonResponse({ |
| 150 | + ok: true, |
| 151 | + conversationId: 'conv-1', |
| 152 | + messageId: 'msg-1', |
| 153 | + conversationCreated: false, |
| 154 | + }) |
| 155 | + ); |
| 156 | + try { |
| 157 | + await postMessageAsUser(VALID_PARAMS); |
| 158 | + const [url] = fetchSpy.mock.calls[0]!; |
| 159 | + expect(url).toBe('https://chat.kiloapps.io/internal/v1/post-message-as-user'); |
| 160 | + expect(warnSpy).toHaveBeenCalledWith( |
| 161 | + expect.stringMatching(/KILO_CHAT_INTERNAL_URL is not set/) |
| 162 | + ); |
| 163 | + } finally { |
| 164 | + process.env.KILO_CHAT_INTERNAL_URL = savedInternal; |
| 165 | + } |
| 166 | + }); |
| 167 | + |
| 168 | + it('throws when neither destination var is configured', async () => { |
| 169 | + const savedInternal = process.env.KILO_CHAT_INTERNAL_URL; |
| 170 | + const savedPublic = process.env.NEXT_PUBLIC_KILO_CHAT_URL; |
| 171 | + delete process.env.KILO_CHAT_INTERNAL_URL; |
118 | 172 | delete process.env.NEXT_PUBLIC_KILO_CHAT_URL; |
119 | 173 | try { |
120 | 174 | await expect(postMessageAsUser(VALID_PARAMS)).rejects.toThrow( |
121 | | - /NEXT_PUBLIC_KILO_CHAT_URL is not configured/ |
| 175 | + /Neither KILO_CHAT_INTERNAL_URL nor NEXT_PUBLIC_KILO_CHAT_URL is configured/ |
122 | 176 | ); |
123 | 177 | } finally { |
124 | | - process.env.NEXT_PUBLIC_KILO_CHAT_URL = saved; |
| 178 | + process.env.KILO_CHAT_INTERNAL_URL = savedInternal; |
| 179 | + process.env.NEXT_PUBLIC_KILO_CHAT_URL = savedPublic; |
125 | 180 | } |
126 | 181 | }); |
127 | 182 |
|
128 | 183 | it('refuses to send the key to an off-allowlist origin (never fetches)', async () => { |
129 | | - const saved = process.env.NEXT_PUBLIC_KILO_CHAT_URL; |
130 | | - process.env.NEXT_PUBLIC_KILO_CHAT_URL = 'https://evil.example.com'; |
| 184 | + const saved = process.env.KILO_CHAT_INTERNAL_URL; |
| 185 | + process.env.KILO_CHAT_INTERNAL_URL = 'https://evil.example.com'; |
131 | 186 | const fetchSpy = jest.spyOn(global, 'fetch'); |
132 | 187 | try { |
133 | 188 | await expect(postMessageAsUser(VALID_PARAMS)).rejects.toThrow( |
134 | 189 | /not an allowed kilo-chat origin/ |
135 | 190 | ); |
136 | 191 | expect(fetchSpy).not.toHaveBeenCalled(); |
137 | 192 | } finally { |
138 | | - process.env.NEXT_PUBLIC_KILO_CHAT_URL = saved; |
| 193 | + process.env.KILO_CHAT_INTERNAL_URL = saved; |
139 | 194 | } |
140 | 195 | }); |
141 | 196 |
|
|
0 commit comments