main
dart 327 lines 9.58 KB
Raw
1 import 'package:flutter_test/flutter_test.dart';
2 import 'package:localmind/core/models/enums.dart';
3 import 'package:localmind/features/servers/data/models/server.dart';
4
5 void main() {
6 group('Server address parsing', () {
7 test('builds baseUrl for a bare host and port', () {
8 final server = Server(
9 id: '1',
10 name: 'Local',
11 type: ServerType.openAICompatible,
12 host: 'localhost',
13 port: 8080,
14 createdAt: DateTime(2026, 1, 1),
15 lastConnectedAt: DateTime(2026, 1, 1),
16 );
17
18 expect(server.baseUrl, 'http://localhost:8080');
19 });
20
21 test('preserves an explicit http URL without doubling the port', () {
22 final server = Server(
23 id: '1',
24 name: 'Local',
25 type: ServerType.openAICompatible,
26 host: 'http://localhost:8080',
27 port: 1234,
28 createdAt: DateTime(2026, 1, 1),
29 lastConnectedAt: DateTime(2026, 1, 1),
30 );
31
32 expect(server.baseUrl, 'http://localhost:8080');
33 expect(server.displayAddress, 'http://localhost:8080');
34 });
35
36 test('accepts https URLs and keeps the configured port when missing', () {
37 final server = Server(
38 id: '1',
39 name: 'Secure',
40 type: ServerType.openAICompatible,
41 host: 'https://localhost',
42 port: 8443,
43 createdAt: DateTime(2026, 1, 1),
44 lastConnectedAt: DateTime(2026, 1, 1),
45 );
46
47 expect(server.baseUrl, 'https://localhost:8443');
48 expect(server.displayAddress, 'https://localhost:8443');
49 });
50
51 test('keeps a host that already includes a port', () {
52 final server = Server(
53 id: '1',
54 name: 'Local',
55 type: ServerType.openAICompatible,
56 host: 'localhost:8080',
57 port: 1234,
58 createdAt: DateTime(2026, 1, 1),
59 lastConnectedAt: DateTime(2026, 1, 1),
60 );
61
62 expect(server.baseUrl, 'http://localhost:8080');
63 expect(server.displayAddress, 'localhost:8080');
64 });
65
66 test('accepts a bare subdomain with no path', () {
67 final server = Server(
68 id: '1',
69 name: 'Remote Ollama',
70 type: ServerType.ollama,
71 host: 'ollama.example.com',
72 port: 11434,
73 createdAt: DateTime(2026, 1, 1),
74 lastConnectedAt: DateTime(2026, 1, 1),
75 );
76
77 expect(server.baseUrl, 'http://ollama.example.com:11434');
78 expect(server.pathPrefix, isNull);
79 });
80
81 test('accepts https subdomain with no path', () {
82 final server = Server(
83 id: '1',
84 name: 'Secure Ollama',
85 type: ServerType.ollama,
86 host: 'https://ollama.example.com',
87 port: 11434,
88 createdAt: DateTime(2026, 1, 1),
89 lastConnectedAt: DateTime(2026, 1, 1),
90 );
91
92 expect(server.baseUrl, 'https://ollama.example.com:11434');
93 expect(server.pathPrefix, isNull);
94 });
95
96 test(
97 'Ollama server with explicit pathPrefix produces correct endpoints',
98 () {
99 final server = Server(
100 id: '1',
101 name: 'Reverse Proxy',
102 type: ServerType.ollama,
103 host: 'https://ai.example.com',
104 port: 11434,
105 pathPrefix: '/ollama',
106 createdAt: DateTime(2026, 1, 1),
107 lastConnectedAt: DateTime(2026, 1, 1),
108 );
109
110 expect(server.baseUrl, 'https://ai.example.com:11434');
111 expect(server.pathPrefix, '/ollama');
112 expect(
113 server.modelsEndpoint,
114 'https://ai.example.com:11434/ollama/api/tags',
115 );
116 expect(
117 server.chatEndpoint,
118 'https://ai.example.com:11434/ollama/api/chat',
119 );
120 },
121 );
122
123 test('trailing slash in pathPrefix is normalized via apiPathPrefix', () {
124 final server = Server(
125 id: '1',
126 name: 'Trailing slash',
127 type: ServerType.ollama,
128 host: 'https://ai.example.com',
129 port: 11434,
130 pathPrefix: '/ollama/',
131 createdAt: DateTime(2026, 1, 1),
132 lastConnectedAt: DateTime(2026, 1, 1),
133 );
134
135 expect(server.apiPathPrefix, '/ollama');
136 expect(
137 server.modelsEndpoint,
138 'https://ai.example.com:11434/ollama/api/tags',
139 );
140 });
141
142 test('root path becomes empty pathPrefix', () {
143 final server = Server(
144 id: '1',
145 name: 'Root path',
146 type: ServerType.ollama,
147 host: 'https://ai.example.com',
148 port: 11434,
149 pathPrefix: '',
150 createdAt: DateTime(2026, 1, 1),
151 lastConnectedAt: DateTime(2026, 1, 1),
152 );
153
154 expect(server.pathPrefix, '');
155 expect(server.baseUrl, 'https://ai.example.com:11434');
156 expect(server.modelsEndpoint, 'https://ai.example.com:11434/api/tags');
157 });
158
159 test('explicit port in host overrides default port', () {
160 final server = Server(
161 id: '1',
162 name: 'Custom port',
163 type: ServerType.ollama,
164 host: 'https://ai.example.com:8443',
165 port: 11434,
166 pathPrefix: '/ollama',
167 createdAt: DateTime(2026, 1, 1),
168 lastConnectedAt: DateTime(2026, 1, 1),
169 );
170
171 expect(server.baseUrl, 'https://ai.example.com:8443');
172 expect(server.pathPrefix, '/ollama');
173 });
174
175 test('OpenAI-compatible with pathPrefix produces correct endpoints', () {
176 final server = Server(
177 id: '1',
178 name: 'OpenAI proxy',
179 type: ServerType.openAICompatible,
180 host: 'https://ai.example.com',
181 port: 8080,
182 pathPrefix: '/openai',
183 createdAt: DateTime(2026, 1, 1),
184 lastConnectedAt: DateTime(2026, 1, 1),
185 );
186
187 expect(server.baseUrl, 'https://ai.example.com:8080');
188 expect(server.pathPrefix, '/openai');
189 expect(
190 server.modelsEndpoint,
191 'https://ai.example.com:8080/openai/v1/models',
192 );
193 expect(
194 server.chatEndpoint,
195 'https://ai.example.com:8080/openai/v1/chat/completions',
196 );
197 expect(server.displayAddress, 'https://ai.example.com:8080/openai');
198 });
199
200 test('OpenAI-compatible with /v1 pathPrefix avoids double /v1', () {
201 final server = Server(
202 id: '1',
203 name: 'OpenAI direct v1',
204 type: ServerType.openAICompatible,
205 host: 'https://ai.example.com',
206 port: 8080,
207 pathPrefix: '/v1',
208 createdAt: DateTime(2026, 1, 1),
209 lastConnectedAt: DateTime(2026, 1, 1),
210 );
211
212 expect(server.modelsEndpoint, 'https://ai.example.com:8080/v1/models');
213 expect(
214 server.chatEndpoint,
215 'https://ai.example.com:8080/v1/chat/completions',
216 );
217 });
218
219 test(
220 'OpenAI-compatible with nested /proxy/v1 pathPrefix avoids double /v1',
221 () {
222 final server = Server(
223 id: '1',
224 name: 'OpenAI nested proxy',
225 type: ServerType.openAICompatible,
226 host: 'https://ai.example.com',
227 port: 8080,
228 pathPrefix: '/proxy/v1',
229 createdAt: DateTime(2026, 1, 1),
230 lastConnectedAt: DateTime(2026, 1, 1),
231 );
232
233 expect(
234 server.modelsEndpoint,
235 'https://ai.example.com:8080/proxy/v1/models',
236 );
237 expect(
238 server.chatEndpoint,
239 'https://ai.example.com:8080/proxy/v1/chat/completions',
240 );
241 },
242 );
243
244 test('LM Studio with pathPrefix produces correct endpoints', () {
245 final server = Server(
246 id: '1',
247 name: 'LM Studio proxy',
248 type: ServerType.lmStudio,
249 host: 'https://ai.example.com',
250 port: 1234,
251 pathPrefix: '/lmstudio',
252 createdAt: DateTime(2026, 1, 1),
253 lastConnectedAt: DateTime(2026, 1, 1),
254 );
255
256 expect(
257 server.modelsEndpoint,
258 'https://ai.example.com:1234/lmstudio/api/v1/models',
259 );
260 expect(
261 server.chatEndpoint,
262 'https://ai.example.com:1234/lmstudio/api/v1/chat',
263 );
264 expect(server.displayAddress, 'https://ai.example.com:1234/lmstudio');
265 });
266
267 test('rejects URL with query string', () {
268 expect(parseServerAddressInput('https://ai.example.com?foo=bar'), isNull);
269 });
270
271 test('rejects URL with fragment', () {
272 expect(parseServerAddressInput('https://ai.example.com#frag'), isNull);
273 });
274
275 test('normalizeServerPathPrefix converts root slash to empty', () {
276 expect(normalizeServerPathPrefix('/'), '');
277 });
278
279 test('normalizeServerPathPrefix strips trailing slash', () {
280 expect(normalizeServerPathPrefix('/ollama/'), '/ollama');
281 });
282
283 test('extractPathPrefix returns empty for no path', () {
284 final uri = Uri.parse('https://example.com');
285 expect(extractPathPrefix(uri), '');
286 });
287
288 test('extractPathPrefix returns empty for root slash', () {
289 final uri = Uri.parse('https://example.com/');
290 expect(extractPathPrefix(uri), '');
291 });
292
293 test('extractPathPrefix returns path for valid prefix', () {
294 final uri = Uri.parse('https://example.com/ollama');
295 expect(extractPathPrefix(uri), '/ollama');
296 });
297
298 test('local IP still works as before', () {
299 final server = Server(
300 id: '1',
301 name: 'Local',
302 type: ServerType.openAICompatible,
303 host: '192.168.1.100',
304 port: 8080,
305 createdAt: DateTime(2026, 1, 1),
306 lastConnectedAt: DateTime(2026, 1, 1),
307 );
308
309 expect(server.baseUrl, 'http://192.168.1.100:8080');
310 expect(server.pathPrefix, isNull);
311 });
312
313 test('existing bare host without port still works', () {
314 final server = Server(
315 id: '1',
316 name: 'Local',
317 type: ServerType.openAICompatible,
318 host: 'localhost',
319 port: 8080,
320 createdAt: DateTime(2026, 1, 1),
321 lastConnectedAt: DateTime(2026, 1, 1),
322 );
323
324 expect(server.baseUrl, 'http://localhost:8080');
325 });
326 });
327 }