1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2012 Nebula, Inc.
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6
# not use this file except in compliance with the License. You may obtain
7
# a copy of the License at
9
# http://www.apache.org/licenses/LICENSE-2.0
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
# License for the specific language governing permissions and limitations
17
from django import http
18
from django.core.urlresolvers import reverse
21
from horizon import api
22
from horizon import test
24
from .workflows import CreateProject, UpdateProject
25
from .views import QUOTA_FIELDS
27
INDEX_URL = reverse('horizon:syspanel:projects:index')
30
class TenantsViewTests(test.BaseAdminViewTests):
32
self.mox.StubOutWithMock(api.keystone, 'tenant_list')
33
api.keystone.tenant_list(IsA(http.HttpRequest), admin=True) \
34
.AndReturn(self.tenants.list())
37
res = self.client.get(INDEX_URL)
38
self.assertTemplateUsed(res, 'syspanel/projects/index.html')
39
self.assertItemsEqual(res.context['table'].data, self.tenants.list())
42
class CreateProjectWorkflowTests(test.BaseAdminViewTests):
43
def _get_project_info(self, project):
44
project_info = {"tenant_name": project.name,
45
"description": project.description,
46
"enabled": project.enabled}
49
def _get_workflow_fields(self, project):
50
project_info = {"name": project.name,
51
"description": project.description,
52
"enabled": project.enabled}
55
def _get_quota_info(self, quota):
57
for field in QUOTA_FIELDS:
58
quota_data[field] = int(getattr(quota, field, None))
61
def _get_workflow_data(self, project, quota):
62
project_info = self._get_workflow_fields(project)
63
quota_data = self._get_quota_info(quota)
64
project_info.update(quota_data)
67
@test.create_stubs({api: ('tenant_quota_defaults',
69
api.keystone: ('user_list',
71
def test_add_project_get(self):
72
quota = self.quotas.first()
73
default_role = self.roles.first()
74
users = self.users.list()
75
roles = self.roles.list()
77
api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \
81
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
82
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
83
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
87
url = reverse('horizon:syspanel:projects:create')
88
res = self.client.get(url)
90
self.assertTemplateUsed(res, 'syspanel/projects/create.html')
92
workflow = res.context['workflow']
93
self.assertEqual(res.context['workflow'].name, CreateProject.name)
95
step = workflow.get_step("createprojectinfoaction")
96
self.assertEqual(step.action.initial['ram'], quota.ram)
97
self.assertEqual(step.action.initial['injected_files'],
99
self.assertQuerysetEqual(workflow.steps,
100
['<CreateProjectInfo: createprojectinfoaction>',
101
'<UpdateProjectMembers: update_members>',
102
'<UpdateProjectQuota: update_quotas>'])
104
@test.create_stubs({api: ('get_default_role',
105
'tenant_quota_defaults',
106
'add_tenant_user_role',),
107
api.keystone: ('tenant_create',
110
api.nova: ('tenant_quota_update',)})
111
def test_add_project_post(self):
112
project = self.tenants.first()
113
quota = self.quotas.first()
114
default_role = self.roles.first()
115
users = self.users.list()
116
roles = self.roles.list()
119
api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \
122
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
123
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
124
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
127
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
130
project_details = self._get_project_info(project)
131
quota_data = self._get_quota_info(quota)
133
api.keystone.tenant_create(IsA(http.HttpRequest), **project_details) \
136
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
140
if "role_" + role.id in workflow_data:
141
ulist = workflow_data["role_" + role.id]
143
api.add_tenant_user_role(IsA(http.HttpRequest),
144
tenant_id=self.tenant.id,
148
api.nova.tenant_quota_update(IsA(http.HttpRequest),
154
workflow_data.update(self._get_workflow_data(project, quota))
156
url = reverse('horizon:syspanel:projects:create')
157
res = self.client.post(url, workflow_data)
159
self.assertNoFormErrors(res)
160
self.assertRedirectsNoFollow(res, INDEX_URL)
162
@test.create_stubs({api: ('tenant_quota_defaults',
163
'get_default_role',),
164
api.keystone: ('user_list',
166
def test_add_project_quota_defaults_error(self):
167
default_role = self.roles.first()
168
users = self.users.list()
169
roles = self.roles.list()
172
api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \
173
.AndRaise(self.exceptions.nova)
175
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
176
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
177
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
181
url = reverse('horizon:syspanel:projects:create')
182
res = self.client.get(url)
184
self.assertTemplateUsed(res, 'syspanel/projects/create.html')
185
self.assertContains(res, "Unable to retrieve default quota values")
187
@test.create_stubs({api: ('get_default_role',
188
'tenant_quota_defaults',),
189
api.keystone: ('tenant_create',
192
def test_add_project_tenant_create_error(self):
193
project = self.tenants.first()
194
quota = self.quotas.first()
195
default_role = self.roles.first()
196
users = self.users.list()
197
roles = self.roles.list()
200
api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \
203
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
204
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
205
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
208
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
211
project_details = self._get_project_info(project)
213
api.keystone.tenant_create(IsA(http.HttpRequest), **project_details) \
214
.AndRaise(self.exceptions.keystone)
218
workflow_data = self._get_workflow_data(project, quota)
220
url = reverse('horizon:syspanel:projects:create')
221
res = self.client.post(url, workflow_data)
223
self.assertNoFormErrors(res)
224
self.assertRedirectsNoFollow(res, INDEX_URL)
226
@test.create_stubs({api: ('get_default_role',
227
'tenant_quota_defaults',
228
'add_tenant_user_role',),
229
api.keystone: ('tenant_create',
232
api.nova: ('tenant_quota_update',)})
233
def test_add_project_quota_update_error(self):
234
project = self.tenants.first()
235
quota = self.quotas.first()
236
default_role = self.roles.first()
237
users = self.users.list()
238
roles = self.roles.list()
241
api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \
244
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
245
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
246
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
249
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
252
project_details = self._get_project_info(project)
253
quota_data = self._get_quota_info(quota)
255
api.keystone.tenant_create(IsA(http.HttpRequest), **project_details) \
258
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
262
if "role_" + role.id in workflow_data:
263
ulist = workflow_data["role_" + role.id]
265
api.add_tenant_user_role(IsA(http.HttpRequest),
266
tenant_id=self.tenant.id,
270
api.nova.tenant_quota_update(IsA(http.HttpRequest),
273
.AndRaise(self.exceptions.nova)
277
workflow_data.update(self._get_workflow_data(project, quota))
279
url = reverse('horizon:syspanel:projects:create')
280
res = self.client.post(url, workflow_data)
282
self.assertNoFormErrors(res)
283
self.assertRedirectsNoFollow(res, INDEX_URL)
285
@test.create_stubs({api: ('get_default_role',
286
'tenant_quota_defaults',
287
'add_tenant_user_role',),
288
api.keystone: ('tenant_create',
291
api.nova: ('tenant_quota_update',)})
292
def test_add_project_user_update_error(self):
293
project = self.tenants.first()
294
quota = self.quotas.first()
295
default_role = self.roles.first()
296
users = self.users.list()
297
roles = self.roles.list()
300
api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \
303
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
304
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
305
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
308
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
311
project_details = self._get_project_info(project)
312
quota_data = self._get_quota_info(quota)
314
api.keystone.tenant_create(IsA(http.HttpRequest), **project_details) \
317
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
321
if "role_" + role.id in workflow_data:
322
ulist = workflow_data["role_" + role.id]
324
api.add_tenant_user_role(IsA(http.HttpRequest),
325
tenant_id=self.tenant.id,
328
.AndRaise(self.exceptions.keystone)
332
api.nova.tenant_quota_update(IsA(http.HttpRequest),
338
workflow_data.update(self._get_workflow_data(project, quota))
340
url = reverse('horizon:syspanel:projects:create')
341
res = self.client.post(url, workflow_data)
343
self.assertNoFormErrors(res)
344
self.assertRedirectsNoFollow(res, INDEX_URL)
346
@test.create_stubs({api: ('get_default_role',
347
'tenant_quota_defaults',),
348
api.keystone: ('user_list',
350
def test_add_project_missing_field_error(self):
351
project = self.tenants.first()
352
quota = self.quotas.first()
353
default_role = self.roles.first()
354
users = self.users.list()
355
roles = self.roles.list()
358
api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \
361
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
362
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
363
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
366
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
370
workflow_data = self._get_workflow_data(project, quota)
371
workflow_data["name"] = ""
373
url = reverse('horizon:syspanel:projects:create')
374
res = self.client.post(url, workflow_data)
376
self.assertContains(res, "field is required")
379
class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
380
def _get_quota_info(self, quota):
382
for field in QUOTA_FIELDS:
383
quota_data[field] = int(getattr(quota, field, None))
386
@test.create_stubs({api: ('get_default_role',
389
'tenant_quota_get',),
390
api.keystone: ('user_list',
392
def test_update_project_get(self):
393
project = self.tenants.first()
394
quota = self.quotas.first()
395
default_role = self.roles.first()
396
users = self.users.list()
397
roles = self.roles.list()
399
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
401
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
404
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
405
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
406
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
409
api.roles_for_user(IsA(http.HttpRequest),
411
self.tenant.id).AndReturn(roles)
415
url = reverse('horizon:syspanel:projects:update',
416
args=[self.tenant.id])
417
res = self.client.get(url)
419
self.assertTemplateUsed(res, 'syspanel/projects/update.html')
421
workflow = res.context['workflow']
422
self.assertEqual(res.context['workflow'].name, UpdateProject.name)
424
step = workflow.get_step("update_info")
425
self.assertEqual(step.action.initial['ram'], quota.ram)
426
self.assertEqual(step.action.initial['injected_files'],
427
quota.injected_files)
428
self.assertEqual(step.action.initial['name'], project.name)
429
self.assertEqual(step.action.initial['description'],
431
self.assertQuerysetEqual(workflow.steps,
432
['<UpdateProjectInfo: update_info>',
433
'<UpdateProjectMembers: update_members>',
434
'<UpdateProjectQuota: update_quotas>'])
436
@test.create_stubs({api: ('tenant_get',
439
'tenant_quota_update',
442
'remove_tenant_user_role',
443
'add_tenant_user_role'),
444
api.keystone: ('user_list',
446
def test_update_project_post(self):
447
project = self.tenants.first()
448
quota = self.quotas.first()
449
default_role = self.roles.first()
450
users = self.users.list()
451
roles = self.roles.list()
452
current_roles = self.roles.list()
455
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
457
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
460
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
461
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
462
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
466
api.roles_for_user(IsA(http.HttpRequest),
468
self.tenant.id).AndReturn(roles)
469
role_ids = [role.id for role in roles]
471
workflow_data.setdefault("role_" + role_ids[0], []) \
475
project._info["name"] = "updated name"
476
project._info["description"] = "updated description"
477
quota.metadata_items = 444
480
updated_project = {"tenant_name": project._info["name"],
481
"tenant_id": project.id,
482
"description": project._info["description"],
483
"enabled": project.enabled}
484
updated_quota = self._get_quota_info(quota)
487
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
490
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
493
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
494
api.keystone.user_list(IsA(http.HttpRequest),
495
tenant_id=self.tenant.id).AndReturn(users)
498
api.roles_for_user(IsA(http.HttpRequest),
501
.AndReturn(current_roles)
503
if "role_" + role.id in workflow_data:
504
ulist = workflow_data["role_" + role.id]
505
if role not in current_roles:
506
api.add_tenant_user_role(IsA(http.HttpRequest),
507
tenant_id=self.tenant.id,
511
current_roles.pop(current_roles.index(role))
512
for to_delete in current_roles:
513
api.remove_tenant_user_role(IsA(http.HttpRequest),
514
tenant_id=self.tenant.id,
516
role_id=to_delete.id)
518
if "role_" + role.id in workflow_data:
519
ulist = workflow_data["role_" + role.id]
521
if not filter(lambda x: user == x.id, users):
522
api.add_tenant_user_role(IsA(http.HttpRequest),
523
tenant_id=self.tenant.id,
527
api.tenant_quota_update(IsA(http.HttpRequest),
534
project_data = {"name": project._info["name"],
536
"description": project._info["description"],
537
"enabled": project.enabled}
538
workflow_data.update(project_data)
539
workflow_data.update(updated_quota)
540
url = reverse('horizon:syspanel:projects:update',
541
args=[self.tenant.id])
542
res = self.client.post(url, workflow_data)
544
self.assertNoFormErrors(res)
545
self.assertRedirectsNoFollow(res, INDEX_URL)
547
@test.create_stubs({api: ('tenant_get',)})
548
def test_update_project_get_error(self):
550
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
551
.AndRaise(self.exceptions.nova)
555
url = reverse('horizon:syspanel:projects:update',
556
args=[self.tenant.id])
557
res = self.client.get(url)
559
self.assertRedirectsNoFollow(res, INDEX_URL)
561
@test.create_stubs({api: ('tenant_get',
564
'tenant_quota_update',
567
'remove_tenant_user',
568
'add_tenant_user_role'),
569
api.keystone: ('user_list',
571
def test_update_project_tenant_update_error(self):
572
project = self.tenants.first()
573
quota = self.quotas.first()
574
default_role = self.roles.first()
575
users = self.users.list()
576
roles = self.roles.list()
579
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
581
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
584
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
585
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
586
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
590
api.roles_for_user(IsA(http.HttpRequest),
592
self.tenant.id).AndReturn(roles)
593
role_ids = [role.id for role in roles]
595
workflow_data.setdefault("role_" + role_ids[0], []) \
599
project._info["name"] = "updated name"
600
project._info["description"] = "updated description"
601
quota.metadata_items = 444
604
updated_project = {"tenant_name": project._info["name"],
605
"tenant_id": project.id,
606
"description": project._info["description"],
607
"enabled": project.enabled}
608
updated_quota = self._get_quota_info(quota)
611
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
614
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
615
.AndRaise(self.exceptions.keystone)
620
project_data = {"name": project._info["name"],
622
"description": project._info["description"],
623
"enabled": project.enabled}
624
workflow_data.update(project_data)
625
workflow_data.update(updated_quota)
626
url = reverse('horizon:syspanel:projects:update',
627
args=[self.tenant.id])
628
res = self.client.post(url, workflow_data)
630
self.assertNoFormErrors(res)
631
self.assertRedirectsNoFollow(res, INDEX_URL)
633
@test.create_stubs({api: ('tenant_get',
636
'tenant_quota_update',
639
'remove_tenant_user_role',
640
'add_tenant_user_role'),
641
api.keystone: ('user_list',
643
def test_update_project_quota_update_error(self):
644
project = self.tenants.first()
645
quota = self.quotas.first()
646
default_role = self.roles.first()
647
users = self.users.list()
648
roles = self.roles.list()
649
current_roles = self.roles.list()
652
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
654
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
657
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
658
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
659
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
663
api.roles_for_user(IsA(http.HttpRequest),
665
self.tenant.id).AndReturn(roles)
666
role_ids = [role.id for role in roles]
668
workflow_data.setdefault("role_" + role_ids[0], []) \
672
project._info["name"] = "updated name"
673
project._info["description"] = "updated description"
674
quota.metadata_items = 444
677
updated_project = {"tenant_name": project._info["name"],
678
"tenant_id": project.id,
679
"description": project._info["description"],
680
"enabled": project.enabled}
681
updated_quota = self._get_quota_info(quota)
684
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
688
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
691
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
692
api.keystone.user_list(IsA(http.HttpRequest),
693
tenant_id=self.tenant.id).AndReturn(users)
696
api.roles_for_user(IsA(http.HttpRequest),
699
.AndReturn(current_roles)
701
if "role_" + role.id in workflow_data:
702
ulist = workflow_data["role_" + role.id]
703
if role not in current_roles:
704
api.add_tenant_user_role(IsA(http.HttpRequest),
705
tenant_id=self.tenant.id,
709
current_roles.pop(current_roles.index(role))
710
for to_delete in current_roles:
711
api.remove_tenant_user_role(IsA(http.HttpRequest),
712
tenant_id=self.tenant.id,
714
role_id=to_delete.id)
716
if "role_" + role.id in workflow_data:
717
ulist = workflow_data["role_" + role.id]
719
if not filter(lambda x: user == x.id, users):
720
api.add_tenant_user_role(IsA(http.HttpRequest),
721
tenant_id=self.tenant.id,
725
api.tenant_quota_update(IsA(http.HttpRequest),
727
**updated_quota).AndRaise(self.exceptions.nova)
732
project_data = {"name": project._info["name"],
734
"description": project._info["description"],
735
"enabled": project.enabled}
736
workflow_data.update(project_data)
737
workflow_data.update(updated_quota)
738
url = reverse('horizon:syspanel:projects:update',
739
args=[self.tenant.id])
740
res = self.client.post(url, workflow_data)
742
self.assertNoFormErrors(res)
743
self.assertRedirectsNoFollow(res, INDEX_URL)
745
@test.create_stubs({api: ('tenant_get',
750
'remove_tenant_user_role',
751
'add_tenant_user_role'),
752
api.keystone: ('user_list',
754
def test_update_project_member_update_error(self):
755
project = self.tenants.first()
756
quota = self.quotas.first()
757
default_role = self.roles.first()
758
users = self.users.list()
759
roles = self.roles.list()
760
current_roles = self.roles.list()
763
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
765
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
768
api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
769
api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
770
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
774
api.roles_for_user(IsA(http.HttpRequest),
776
self.tenant.id).AndReturn(roles)
777
role_ids = [role.id for role in roles]
779
workflow_data.setdefault("role_" + role_ids[0], []) \
783
project._info["name"] = "updated name"
784
project._info["description"] = "updated description"
785
quota.metadata_items = 444
788
updated_project = {"tenant_name": project._info["name"],
789
"tenant_id": project.id,
790
"description": project._info["description"],
791
"enabled": project.enabled}
792
updated_quota = self._get_quota_info(quota)
795
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
798
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
801
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
802
api.keystone.user_list(IsA(http.HttpRequest),
803
tenant_id=self.tenant.id).AndReturn(users)
806
api.roles_for_user(IsA(http.HttpRequest),
809
.AndReturn(current_roles)
811
if "role_" + role.id in workflow_data:
812
if role not in current_roles:
813
api.add_tenant_user_role(IsA(http.HttpRequest),
814
tenant_id=self.tenant.id,
818
current_roles.pop(current_roles.index(role))
819
for to_delete in current_roles:
820
api.remove_tenant_user_role(IsA(http.HttpRequest),
821
tenant_id=self.tenant.id,
823
role_id=to_delete.id) \
824
.AndRaise(self.exceptions.nova)
831
project_data = {"name": project._info["name"],
833
"description": project._info["description"],
834
"enabled": project.enabled}
835
workflow_data.update(project_data)
836
workflow_data.update(updated_quota)
837
url = reverse('horizon:syspanel:projects:update',
838
args=[self.tenant.id])
839
res = self.client.post(url, workflow_data)
841
self.assertNoFormErrors(res)
842
self.assertRedirectsNoFollow(res, INDEX_URL)