Merge ~lloydwaltersj/maas:v3-resourcepool into maas:master
- Git
- lp:~lloydwaltersj/maas
- v3-resourcepool
- Merge into master
Status: | Merged |
---|---|
Approved by: | Jacopo Rota |
Approved revision: | ce7dbdb0c63d707aae1179d0ddb24e9c0e282ed8 |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~lloydwaltersj/maas:v3-resourcepool |
Merge into: | maas:master |
Diff against target: |
1369 lines (+1133/-21) 22 files modified
src/maasapiserver/common/models/constants.py (+1/-0) src/maasapiserver/v3/api/handlers/__init__.py (+7/-1) src/maasapiserver/v3/api/handlers/resource_pools.py (+151/-0) src/maasapiserver/v3/api/models/requests/base.py (+17/-0) src/maasapiserver/v3/api/models/requests/resource_pools.py (+25/-0) src/maasapiserver/v3/api/models/responses/resource_pools.py (+20/-0) src/maasapiserver/v3/db/base.py (+1/-1) src/maasapiserver/v3/db/bmc.py (+1/-1) src/maasapiserver/v3/db/nodes.py (+1/-1) src/maasapiserver/v3/db/resource_pools.py (+112/-0) src/maasapiserver/v3/db/users.py (+1/-1) src/maasapiserver/v3/db/vmcluster.py (+1/-1) src/maasapiserver/v3/db/zones.py (+1/-1) src/maasapiserver/v3/models/resource_pools.py (+24/-0) src/maasapiserver/v3/services/__init__.py (+3/-0) src/maasapiserver/v3/services/resource_pools.py (+65/-0) src/tests/fixtures/factories/resource_pools.py (+49/-0) src/tests/maasapiserver/v3/api/models/requests/test_base.py (+42/-14) src/tests/maasapiserver/v3/api/test_resource_pools.py (+275/-0) src/tests/maasapiserver/v3/db/test_resource_pools.py (+149/-0) src/tests/maasapiserver/v3/models/test_resource_pools.py (+36/-0) src/tests/maasapiserver/v3/services/test_resource_pools.py (+151/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jacopo Rota | Approve | ||
MAAS Lander | Approve | ||
Review via email: mp+461550@code.launchpad.net |
Commit message
Add Resource Pools v3 Endpoints
Description of the change
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 51ede793710c2fd
Jack Lloyd-Walters (lloydwaltersj) wrote : | # |
jenkins: !test
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 51ede793710c2fd
- 3d9899e... by Jack Lloyd-Walters
-
linting
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 921a2d222087fdc
- 1326ddf... by Jack Lloyd-Walters
-
remove f string
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 5fb62af3e212cd3
- e1411df... by Jack Lloyd-Walters
-
move imports
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 770baa4f76eac0a
- 9da1baa... by Jack Lloyd-Walters
-
import flip
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 1ff003b893636af
- 2a311c3... by Jack Lloyd-Walters
-
fixture in the wrong place
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: SUCCESS
COMMIT: b0db01d80a43591
Jacopo Rota (r00ta) wrote : | # |
Nice! I have some minor comments/fixes that should be easy to fix. In the meanwhile I'll check what do we need to do for the `delete` as we might have to migrate some resources attached to the resource pool being deleted. I'll get back to you
- ce6dc56... by Jack Lloyd-Walters
-
linting and responding to feedback
- 126af19... by Jack Lloyd-Walters
-
await
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 2af04d35231ecff
- b2e94c2... by Jack Lloyd-Walters
-
pass none to optional
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 37d136f2b58f7f8
- 5ebffb4... by Jack Lloyd-Walters
-
Squashed commit of the following:
commit 8b07c3d5c23d179
841c8ede1515564 d1d418bce0
Author: Christian Grabowski <email address hidden>
Date: Tue Mar 5 14:58:34 2024 +0000chore: move maas-temporal-
worker to its own package commit dab4e93190055f1
98d5f793206d8ca 90f1cf6bc6
Author: Peter Makowski <email address hidden>
Date: Tue Mar 5 14:34:46 2024 +0000Update maas-ui to 1fd5c1212
build(dotrun): remove start_django_db (#5331)commit 85fc8efc48b6b58
136c48b7e28ea64 6be3358836
Author: Alexsander Silva de Souza <email address hidden>
Date: Tue Mar 5 14:25:41 2024 +0000restore target used by maas-release-tools
(cherry picked from commit 53d5ac1f4f782f8
f59f399dc3240f8 b4a26bc6b4) commit c6b3d8df0113121
0abe153db6b48a9 bf087a5cca
Author: Javier Fuentes <email address hidden>
Date: Tue Mar 5 12:52:58 2024 +0000fix: update name of the Django default engine database
commit e885d22fb7a6454
90987129fb38aa7 3067a01ee1
Author: Jacopo Rota <email address hidden>
Date: Tue Mar 5 10:55:16 2024 +0000MAASENG-2807: add secrets and configurations services in V3 maasapiserver
- 3957e09... by Jack Lloyd-Walters
-
linting
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: fbec37a509471d2
- c894cb8... by Jack Lloyd-Walters
-
Squashed commit of the following:
commit 8b07c3d5c23d179
841c8ede1515564 d1d418bce0
Author: Christian Grabowski <email address hidden>
Date: Tue Mar 5 14:58:34 2024 +0000chore: move maas-temporal-
worker to its own package commit dab4e93190055f1
98d5f793206d8ca 90f1cf6bc6
Author: Peter Makowski <email address hidden>
Date: Tue Mar 5 14:34:46 2024 +0000Update maas-ui to 1fd5c1212
build(dotrun): remove start_django_db (#5331)commit 85fc8efc48b6b58
136c48b7e28ea64 6be3358836
Author: Alexsander Silva de Souza <email address hidden>
Date: Tue Mar 5 14:25:41 2024 +0000restore target used by maas-release-tools
(cherry picked from commit 53d5ac1f4f782f8
f59f399dc3240f8 b4a26bc6b4) commit c6b3d8df0113121
0abe153db6b48a9 bf087a5cca
Author: Javier Fuentes <email address hidden>
Date: Tue Mar 5 12:52:58 2024 +0000fix: update name of the Django default engine database
commit e885d22fb7a6454
90987129fb38aa7 3067a01ee1
Author: Jacopo Rota <email address hidden>
Date: Tue Mar 5 10:55:16 2024 +0000MAASENG-2807: add secrets and configurations services in V3 maasapiserver
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 51bd3f16508a463
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: SUCCESS
COMMIT: 66199f987ca9a77
Jacopo Rota (r00ta) wrote : | # |
some minor comments
- 3bd403c... by Jack Lloyd-Walters
-
respond to feedback
Jack Lloyd-Walters (lloydwaltersj) : | # |
- a890253... by Jack Lloyd-Walters
-
linting and formatting
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: c7d5e0681def6fc
- db054aa... by Jack Lloyd-Walters
-
correct error message
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: ae562bd642fa669
- 934047c... by Jack Lloyd-Walters
-
rebase
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 934047c337694ed
- ffad2ec... by Jack Lloyd-Walters
-
fix tests
Jack Lloyd-Walters (lloydwaltersj) wrote : | # |
jenkins: !test
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: ffad2ec3cc64428
Jack Lloyd-Walters (lloydwaltersj) wrote : | # |
jenkins: !test
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: ffad2ec3cc64428
- 56e1232... by Jacopo Rota
-
minor naming fixes, change BaseRepository generics, move update logic to service, add tests for api and service
- 062fe54... by Jacopo Rota
-
fix typos
- b384579... by Jacopo Rota
-
refactoring
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: b3845797f3e59b0
Jacopo Rota (r00ta) wrote : | # |
Jenkins: !test
Jacopo Rota (r00ta) wrote : | # |
jenkins: !test
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: b3845797f3e59b0
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 73dc28489332148
- 65b5f99... by Jack Lloyd-Walters
-
submodule fix?
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 65b5f99706b4528
- b912594... by Jacopo Rota
-
update git submodules
- 2f6eae8... by Jacopo Rota
-
Merge remote-tracking branch 'upstream/master' into HEAD
- ce7dbdb... by Jack Lloyd-Walters
-
Merge remote-tracking branch 'r00ta/
v3-resource- pool-rebase' into v3-resourcepool
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: SUCCESS
COMMIT: ce7dbdb0c63d707
Preview Diff
1 | diff --git a/src/maasapiserver/common/models/constants.py b/src/maasapiserver/common/models/constants.py |
2 | index d5554a3..442c566 100644 |
3 | --- a/src/maasapiserver/common/models/constants.py |
4 | +++ b/src/maasapiserver/common/models/constants.py |
5 | @@ -8,6 +8,7 @@ UNEXISTING_USER_OR_INVALID_CREDENTIALS_VIOLATION_TYPE = ( |
6 | # Generic |
7 | UNIQUE_CONSTRAINT_VIOLATION_TYPE = "UniqueConstraintViolation" |
8 | ETAG_PRECONDITION_VIOLATION_TYPE = "EtagPreconditionViolation" |
9 | +UNEXISTING_RESOURCE_VIOLATION_TYPE = "UnexistingResourceViolation" |
10 | |
11 | # Zones |
12 | CANNOT_DELETE_DEFAULT_ZONE_VIOLATION_TYPE = "CannotDeleteDefaultZoneViolation" |
13 | diff --git a/src/maasapiserver/v3/api/handlers/__init__.py b/src/maasapiserver/v3/api/handlers/__init__.py |
14 | index 56c6bd8..e28615e 100644 |
15 | --- a/src/maasapiserver/v3/api/handlers/__init__.py |
16 | +++ b/src/maasapiserver/v3/api/handlers/__init__.py |
17 | @@ -1,10 +1,16 @@ |
18 | from maasapiserver.common.api.base import API |
19 | from maasapiserver.v3.api.handlers.auth import AuthHandler |
20 | +from maasapiserver.v3.api.handlers.resource_pools import ResourcePoolHandler |
21 | from maasapiserver.v3.api.handlers.root import RootHandler |
22 | from maasapiserver.v3.api.handlers.zones import ZonesHandler |
23 | from maasapiserver.v3.constants import V3_API_PREFIX |
24 | |
25 | APIv3 = API( |
26 | prefix=V3_API_PREFIX, |
27 | - handlers=[RootHandler(), ZonesHandler(), AuthHandler()], |
28 | + handlers=[ |
29 | + RootHandler(), |
30 | + ZonesHandler(), |
31 | + ResourcePoolHandler(), |
32 | + AuthHandler(), |
33 | + ], |
34 | ) |
35 | diff --git a/src/maasapiserver/v3/api/handlers/resource_pools.py b/src/maasapiserver/v3/api/handlers/resource_pools.py |
36 | new file mode 100644 |
37 | index 0000000..32b0899 |
38 | --- /dev/null |
39 | +++ b/src/maasapiserver/v3/api/handlers/resource_pools.py |
40 | @@ -0,0 +1,151 @@ |
41 | +from fastapi import Depends, Response |
42 | + |
43 | +from maasapiserver.common.api.base import Handler, handler |
44 | +from maasapiserver.common.api.models.responses.errors import ( |
45 | + ConflictBodyResponse, |
46 | + NotFoundBodyResponse, |
47 | + NotFoundResponse, |
48 | + ValidationErrorBodyResponse, |
49 | +) |
50 | +from maasapiserver.v3.api import services |
51 | +from maasapiserver.v3.api.models.requests.query import PaginationParams |
52 | +from maasapiserver.v3.api.models.requests.resource_pools import ( |
53 | + ResourcePoolPatchRequest, |
54 | + ResourcePoolRequest, |
55 | +) |
56 | +from maasapiserver.v3.api.models.responses.resource_pools import ( |
57 | + ResourcePoolResponse, |
58 | + ResourcePoolsListResponse, |
59 | +) |
60 | +from maasapiserver.v3.constants import EXTERNAL_V3_API_PREFIX |
61 | +from maasapiserver.v3.services import ServiceCollectionV3 |
62 | + |
63 | + |
64 | +class ResourcePoolHandler(Handler): |
65 | + """ResourcePool API handler.""" |
66 | + |
67 | + TAGS = ["ResourcePool"] |
68 | + |
69 | + @handler( |
70 | + path="/resource_pools", |
71 | + methods=["GET"], |
72 | + tags=TAGS, |
73 | + responses={ |
74 | + 200: { |
75 | + "model": ResourcePoolsListResponse, |
76 | + }, |
77 | + 422: {"model": ValidationErrorBodyResponse}, |
78 | + }, |
79 | + status_code=200, |
80 | + response_model_exclude_none=True, |
81 | + ) |
82 | + async def list_resource_pools( |
83 | + self, |
84 | + pagination_params: PaginationParams = Depends(), |
85 | + services: ServiceCollectionV3 = Depends(services), |
86 | + ) -> Response: |
87 | + resource_pools = await services.resource_pools.list(pagination_params) |
88 | + return ResourcePoolsListResponse( |
89 | + items=[ |
90 | + resource_pools.to_response( |
91 | + f"{EXTERNAL_V3_API_PREFIX}/resource_pools" |
92 | + ) |
93 | + for resource_pools in resource_pools.items |
94 | + ], |
95 | + total=resource_pools.total, |
96 | + ) |
97 | + |
98 | + @handler( |
99 | + path="/resource_pools", |
100 | + methods=["POST"], |
101 | + tags=TAGS, |
102 | + responses={ |
103 | + 201: { |
104 | + "model": ResourcePoolResponse, |
105 | + "headers": { |
106 | + "ETag": {"description": "The ETag for the resource"} |
107 | + }, |
108 | + }, |
109 | + 409: {"model": ConflictBodyResponse}, |
110 | + 422: {"model": ValidationErrorBodyResponse}, |
111 | + }, |
112 | + status_code=201, |
113 | + response_model_exclude_none=True, |
114 | + ) |
115 | + async def create_resource_pool( |
116 | + self, |
117 | + response: Response, |
118 | + resource_pool_request: ResourcePoolRequest, |
119 | + services: ServiceCollectionV3 = Depends(services), |
120 | + ) -> Response: |
121 | + resource_pools = await services.resource_pools.create( |
122 | + resource_pool_request |
123 | + ) |
124 | + response.headers["ETag"] = resource_pools.etag() |
125 | + return resource_pools.to_response( |
126 | + self_base_hyperlink=f"{EXTERNAL_V3_API_PREFIX}/resource_pools" |
127 | + ) |
128 | + |
129 | + @handler( |
130 | + path="/resource_pools/{resource_pool_id}", |
131 | + methods=["GET"], |
132 | + tags=TAGS, |
133 | + responses={ |
134 | + 200: { |
135 | + "model": ResourcePoolResponse, |
136 | + "headers": { |
137 | + "ETag": {"description": "The ETag for the resource"} |
138 | + }, |
139 | + }, |
140 | + 404: {"model": NotFoundBodyResponse}, |
141 | + 422: {"model": ValidationErrorBodyResponse}, |
142 | + }, |
143 | + status_code=200, |
144 | + response_model_exclude_none=True, |
145 | + ) |
146 | + async def get_resource_pool( |
147 | + self, |
148 | + resource_pool_id: int, |
149 | + response: Response, |
150 | + services: ServiceCollectionV3 = Depends(services), |
151 | + ) -> Response: |
152 | + if resource_pool := await services.resource_pools.get_by_id( |
153 | + resource_pool_id |
154 | + ): |
155 | + response.headers["ETag"] = resource_pool.etag() |
156 | + return resource_pool.to_response( |
157 | + self_base_hyperlink=f"{EXTERNAL_V3_API_PREFIX}/resource_pools" |
158 | + ) |
159 | + return NotFoundResponse() |
160 | + |
161 | + @handler( |
162 | + path="/resource_pools/{resource_pool_id}", |
163 | + methods=["PATCH"], |
164 | + tags=TAGS, |
165 | + responses={ |
166 | + 200: { |
167 | + "model": ResourcePoolResponse, |
168 | + "headers": { |
169 | + "ETag": {"description": "The ETag for the resource"} |
170 | + }, |
171 | + }, |
172 | + 404: {"model": NotFoundBodyResponse}, |
173 | + 422: {"model": ValidationErrorBodyResponse}, |
174 | + }, |
175 | + status_code=200, |
176 | + response_model_exclude_none=True, |
177 | + ) |
178 | + async def patch_resource_pool( |
179 | + self, |
180 | + resource_pool_id: int, |
181 | + response: Response, |
182 | + resource_pool_request: ResourcePoolPatchRequest, |
183 | + services: ServiceCollectionV3 = Depends(services), |
184 | + ) -> Response: |
185 | + resource_pool = await services.resource_pools.patch( |
186 | + resource_pool_id, resource_pool_request |
187 | + ) |
188 | + response.headers["ETag"] = resource_pool.etag() |
189 | + return resource_pool.to_response( |
190 | + self_base_hyperlink=f"{EXTERNAL_V3_API_PREFIX}/resource_pools" |
191 | + ) |
192 | diff --git a/src/maasapiserver/v3/api/models/requests/base.py b/src/maasapiserver/v3/api/models/requests/base.py |
193 | index a674341..df507c2 100644 |
194 | --- a/src/maasapiserver/v3/api/models/requests/base.py |
195 | +++ b/src/maasapiserver/v3/api/models/requests/base.py |
196 | @@ -1,4 +1,5 @@ |
197 | from re import compile |
198 | +from typing import Optional |
199 | |
200 | from pydantic import BaseModel, Field, validator |
201 | |
202 | @@ -15,3 +16,19 @@ class NamedBaseModel(BaseModel): |
203 | if not MODEL_NAME_VALIDATOR.match(v): |
204 | raise ValueError("Invalid entity name.") |
205 | return v |
206 | + |
207 | + |
208 | +class OptionalNamedBaseModel(BaseModel): |
209 | + name: Optional[str] = Field( |
210 | + description="The unique name of the entity.", default=None |
211 | + ) |
212 | + |
213 | + # TODO: move to @field_validator when we migrate to pydantic 2.x |
214 | + @validator("name") |
215 | + def check_regex_name(cls, v: str) -> str: |
216 | + # If the name is set, it must not be None and it must match the regex |
217 | + if v is None: |
218 | + raise ValueError("The name for the resource must not be null.") |
219 | + if not MODEL_NAME_VALIDATOR.match(v): |
220 | + raise ValueError("Invalid entity name.") |
221 | + return v |
222 | diff --git a/src/maasapiserver/v3/api/models/requests/resource_pools.py b/src/maasapiserver/v3/api/models/requests/resource_pools.py |
223 | new file mode 100644 |
224 | index 0000000..6c3bd8f |
225 | --- /dev/null |
226 | +++ b/src/maasapiserver/v3/api/models/requests/resource_pools.py |
227 | @@ -0,0 +1,25 @@ |
228 | +from typing import Optional |
229 | + |
230 | +from pydantic import validator |
231 | + |
232 | +from maasapiserver.v3.api.models.requests.base import ( |
233 | + NamedBaseModel, |
234 | + OptionalNamedBaseModel, |
235 | +) |
236 | + |
237 | + |
238 | +class ResourcePoolRequest(NamedBaseModel): |
239 | + description: str |
240 | + |
241 | + |
242 | +class ResourcePoolPatchRequest(OptionalNamedBaseModel): |
243 | + description: Optional[str] |
244 | + |
245 | + @validator("description") |
246 | + def check_description(cls, v: str) -> str: |
247 | + # If the description is set in the request, it must not be None |
248 | + if v is None: |
249 | + raise ValueError( |
250 | + "The description for the resource must not be null." |
251 | + ) |
252 | + return v |
253 | diff --git a/src/maasapiserver/v3/api/models/responses/resource_pools.py b/src/maasapiserver/v3/api/models/responses/resource_pools.py |
254 | new file mode 100644 |
255 | index 0000000..a90bc87 |
256 | --- /dev/null |
257 | +++ b/src/maasapiserver/v3/api/models/responses/resource_pools.py |
258 | @@ -0,0 +1,20 @@ |
259 | +from datetime import datetime |
260 | + |
261 | +from maasapiserver.v3.api.models.responses.base import ( |
262 | + BaseHal, |
263 | + HalResponse, |
264 | + PaginatedResponse, |
265 | +) |
266 | + |
267 | + |
268 | +class ResourcePoolResponse(HalResponse[BaseHal]): |
269 | + kind = "ResourcePool" |
270 | + id: int |
271 | + name: str |
272 | + description: str |
273 | + created: datetime |
274 | + updated: datetime |
275 | + |
276 | + |
277 | +class ResourcePoolsListResponse(PaginatedResponse[ResourcePoolResponse]): |
278 | + kind = "ResourcePoolList" |
279 | diff --git a/src/maasapiserver/v3/db/base.py b/src/maasapiserver/v3/db/base.py |
280 | index 36dd883..21ff3cf 100644 |
281 | --- a/src/maasapiserver/v3/db/base.py |
282 | +++ b/src/maasapiserver/v3/db/base.py |
283 | @@ -28,7 +28,7 @@ class BaseRepository(ABC, Generic[T, K]): |
284 | pass |
285 | |
286 | @abstractmethod |
287 | - async def update(self, id: int, request: K) -> T: |
288 | + async def update(self, resource: T) -> T: |
289 | pass |
290 | |
291 | @abstractmethod |
292 | diff --git a/src/maasapiserver/v3/db/bmc.py b/src/maasapiserver/v3/db/bmc.py |
293 | index b6ac49a..7dee695 100644 |
294 | --- a/src/maasapiserver/v3/db/bmc.py |
295 | +++ b/src/maasapiserver/v3/db/bmc.py |
296 | @@ -21,7 +21,7 @@ class BmcRepository(BaseRepository[Bmc, BmcRequest]): |
297 | ) -> ListResult[Bmc]: |
298 | raise Exception("Not implemented yet.") |
299 | |
300 | - async def update(self, id: int, request: BmcRequest) -> Bmc: |
301 | + async def update(self, resource: Bmc) -> Bmc: |
302 | raise Exception("Not implemented yet.") |
303 | |
304 | async def delete(self, id: int) -> None: |
305 | diff --git a/src/maasapiserver/v3/db/nodes.py b/src/maasapiserver/v3/db/nodes.py |
306 | index 04599ed..2686a7a 100644 |
307 | --- a/src/maasapiserver/v3/db/nodes.py |
308 | +++ b/src/maasapiserver/v3/db/nodes.py |
309 | @@ -21,7 +21,7 @@ class NodesRepository(BaseRepository[Node, NodeRequest]): |
310 | ) -> ListResult[Node]: |
311 | raise Exception("Not implemented yet.") |
312 | |
313 | - async def update(self, id: int, request: NodeRequest) -> Node: |
314 | + async def update(self, resource: Node) -> Node: |
315 | raise Exception("Not implemented yet.") |
316 | |
317 | async def delete(self, id: int) -> None: |
318 | diff --git a/src/maasapiserver/v3/db/resource_pools.py b/src/maasapiserver/v3/db/resource_pools.py |
319 | new file mode 100644 |
320 | index 0000000..33640ae |
321 | --- /dev/null |
322 | +++ b/src/maasapiserver/v3/db/resource_pools.py |
323 | @@ -0,0 +1,112 @@ |
324 | +from datetime import datetime |
325 | +from typing import Any, Optional |
326 | + |
327 | +from sqlalchemy import desc, insert, select, Select, update |
328 | +from sqlalchemy.exc import IntegrityError |
329 | +from sqlalchemy.sql.functions import count |
330 | +from sqlalchemy.sql.operators import eq |
331 | + |
332 | +from maasapiserver.common.db.tables import ResourcePoolTable |
333 | +from maasapiserver.common.models.constants import ( |
334 | + UNIQUE_CONSTRAINT_VIOLATION_TYPE, |
335 | +) |
336 | +from maasapiserver.common.models.exceptions import ( |
337 | + AlreadyExistsException, |
338 | + BaseExceptionDetail, |
339 | +) |
340 | +from maasapiserver.v3.api.models.requests.query import PaginationParams |
341 | +from maasapiserver.v3.api.models.requests.resource_pools import ( |
342 | + ResourcePoolRequest, |
343 | +) |
344 | +from maasapiserver.v3.db.base import BaseRepository |
345 | +from maasapiserver.v3.models.base import ListResult |
346 | +from maasapiserver.v3.models.resource_pools import ResourcePool |
347 | + |
348 | +RESOURCE_POOLS_FIELDS = ( |
349 | + ResourcePoolTable.c.id, |
350 | + ResourcePoolTable.c.name, |
351 | + ResourcePoolTable.c.description, |
352 | + ResourcePoolTable.c.created, |
353 | + ResourcePoolTable.c.updated, |
354 | +) |
355 | + |
356 | + |
357 | +class ResourcePoolRepository( |
358 | + BaseRepository[ResourcePool, ResourcePoolRequest] |
359 | +): |
360 | + async def find_by_id(self, id: int) -> Optional[ResourcePool]: |
361 | + stmt = self._select_all_statement().where( |
362 | + eq(ResourcePoolTable.c.id, id) |
363 | + ) |
364 | + if result := await self.connection.execute(stmt): |
365 | + if resource_pools := result.one_or_none(): |
366 | + return ResourcePool(**resource_pools._asdict()) |
367 | + return None |
368 | + |
369 | + async def create(self, request: ResourcePoolRequest) -> ResourcePool: |
370 | + now = datetime.utcnow() |
371 | + stmt = ( |
372 | + insert(ResourcePoolTable) |
373 | + .returning(*RESOURCE_POOLS_FIELDS) |
374 | + .values( |
375 | + name=request.name, |
376 | + description=request.description, |
377 | + updated=now, |
378 | + created=now, |
379 | + ) |
380 | + ) |
381 | + try: |
382 | + result = await self.connection.execute(stmt) |
383 | + except IntegrityError: |
384 | + self._raise_constraint_violation(request.name) |
385 | + resource_pools = result.one() |
386 | + return ResourcePool(**resource_pools._asdict()) |
387 | + |
388 | + async def list( |
389 | + self, pagination_params: PaginationParams |
390 | + ) -> ListResult[ResourcePool]: |
391 | + total_stmt = select(count()).select_from(ResourcePoolTable) |
392 | + total = (await self.connection.execute(total_stmt)).scalar() |
393 | + |
394 | + stmt = ( |
395 | + self._select_all_statement() |
396 | + .order_by(desc(ResourcePoolTable.c.id)) |
397 | + .offset((pagination_params.page - 1) * pagination_params.size) |
398 | + .limit(pagination_params.size) |
399 | + ) |
400 | + |
401 | + result = await self.connection.execute(stmt) |
402 | + return ListResult[ResourcePool]( |
403 | + items=[ResourcePool(**row._asdict()) for row in result.all()], |
404 | + total=total, |
405 | + ) |
406 | + |
407 | + async def delete(self, id: int) -> None: |
408 | + raise Exception("Not implemented yet.") |
409 | + |
410 | + async def update(self, resource_pool: ResourcePool) -> ResourcePool: |
411 | + resource_pool.updated = datetime.utcnow() |
412 | + stmt = ( |
413 | + update(ResourcePoolTable) |
414 | + .where(eq(ResourcePoolTable.c.id, resource_pool.id)) |
415 | + .returning(*RESOURCE_POOLS_FIELDS) |
416 | + .values(**resource_pool.dict()) |
417 | + ) |
418 | + try: |
419 | + new_resource_pool = (await self.connection.execute(stmt)).one() |
420 | + except IntegrityError: |
421 | + self._raise_constraint_violation(resource_pool.name) |
422 | + return ResourcePool(**new_resource_pool._asdict()) |
423 | + |
424 | + def _select_all_statement(self) -> Select[Any]: |
425 | + return select(*RESOURCE_POOLS_FIELDS).select_from(ResourcePoolTable) |
426 | + |
427 | + def _raise_constraint_violation(self, name: str): |
428 | + raise AlreadyExistsException( |
429 | + details=[ |
430 | + BaseExceptionDetail( |
431 | + type=UNIQUE_CONSTRAINT_VIOLATION_TYPE, |
432 | + message=f"An entity named '{name}' already exists.", |
433 | + ) |
434 | + ] |
435 | + ) |
436 | diff --git a/src/maasapiserver/v3/db/users.py b/src/maasapiserver/v3/db/users.py |
437 | index c058284..573b790 100644 |
438 | --- a/src/maasapiserver/v3/db/users.py |
439 | +++ b/src/maasapiserver/v3/db/users.py |
440 | @@ -32,7 +32,7 @@ class UsersRepository(BaseRepository[User, UserRequest]): |
441 | ) -> ListResult[User]: |
442 | raise Exception("Not implemented yet.") |
443 | |
444 | - async def update(self, id: int, request: UserRequest) -> User: |
445 | + async def update(self, resource: User) -> User: |
446 | raise Exception("Not implemented yet.") |
447 | |
448 | async def delete(self, id: int) -> None: |
449 | diff --git a/src/maasapiserver/v3/db/vmcluster.py b/src/maasapiserver/v3/db/vmcluster.py |
450 | index 76698f7..b0a2bbf 100644 |
451 | --- a/src/maasapiserver/v3/db/vmcluster.py |
452 | +++ b/src/maasapiserver/v3/db/vmcluster.py |
453 | @@ -21,7 +21,7 @@ class VmClustersRepository(BaseRepository[VmCluster, VmClusterRequest]): |
454 | ) -> ListResult[VmCluster]: |
455 | raise Exception("Not implemented yet.") |
456 | |
457 | - async def update(self, id: int, request: VmClusterRequest) -> VmCluster: |
458 | + async def update(self, resource: VmCluster) -> VmCluster: |
459 | raise Exception("Not implemented yet.") |
460 | |
461 | async def delete(self, id: int) -> None: |
462 | diff --git a/src/maasapiserver/v3/db/zones.py b/src/maasapiserver/v3/db/zones.py |
463 | index c9a4566..5325896 100644 |
464 | --- a/src/maasapiserver/v3/db/zones.py |
465 | +++ b/src/maasapiserver/v3/db/zones.py |
466 | @@ -99,7 +99,7 @@ class ZonesRepository(BaseRepository[Zone, ZoneRequest]): |
467 | items=[Zone(**row._asdict()) for row in result.all()], total=total |
468 | ) |
469 | |
470 | - async def update(self, id: int, request: ZoneRequest) -> Zone: |
471 | + async def update(self, resource: Zone) -> Zone: |
472 | raise Exception("Not implemented yet.") |
473 | |
474 | async def delete(self, id: int) -> None: |
475 | diff --git a/src/maasapiserver/v3/models/resource_pools.py b/src/maasapiserver/v3/models/resource_pools.py |
476 | new file mode 100644 |
477 | index 0000000..722b47e |
478 | --- /dev/null |
479 | +++ b/src/maasapiserver/v3/models/resource_pools.py |
480 | @@ -0,0 +1,24 @@ |
481 | +from maasapiserver.v3.api.models.responses.base import BaseHal, BaseHref |
482 | +from maasapiserver.v3.api.models.responses.resource_pools import ( |
483 | + ResourcePoolResponse, |
484 | +) |
485 | +from maasapiserver.v3.models.base import MaasTimestampedBaseModel |
486 | + |
487 | + |
488 | +class ResourcePool(MaasTimestampedBaseModel): |
489 | + name: str |
490 | + description: str |
491 | + |
492 | + def to_response(self, self_base_hyperlink: str) -> ResourcePoolResponse: |
493 | + return ResourcePoolResponse( |
494 | + id=self.id, |
495 | + name=self.name, |
496 | + description=self.description, |
497 | + created=self.created, |
498 | + updated=self.updated, |
499 | + hal_links=BaseHal( |
500 | + self=BaseHref( |
501 | + href=f"{self_base_hyperlink.rstrip('/')}/{self.id}" |
502 | + ) |
503 | + ), |
504 | + ) |
505 | diff --git a/src/maasapiserver/v3/services/__init__.py b/src/maasapiserver/v3/services/__init__.py |
506 | index 696a3fe..2eac400 100644 |
507 | --- a/src/maasapiserver/v3/services/__init__.py |
508 | +++ b/src/maasapiserver/v3/services/__init__.py |
509 | @@ -4,6 +4,7 @@ from maasapiserver.v3.services.auth import AuthService |
510 | from maasapiserver.v3.services.bmc import BmcService |
511 | from maasapiserver.v3.services.configurations import ConfigurationsService |
512 | from maasapiserver.v3.services.nodes import NodesService |
513 | +from maasapiserver.v3.services.resource_pools import ResourcePoolsService |
514 | from maasapiserver.v3.services.secrets import ( |
515 | SecretsService, |
516 | SecretsServiceFactory, |
517 | @@ -22,6 +23,7 @@ class ServiceCollectionV3: |
518 | zones: ZonesService |
519 | secrets: SecretsService |
520 | configurations: ConfigurationsService |
521 | + resource_pools: ResourcePoolsService |
522 | auth: AuthService |
523 | |
524 | @classmethod |
525 | @@ -48,4 +50,5 @@ class ServiceCollectionV3: |
526 | vmcluster_service=services.vmclusters, |
527 | bmc_service=services.bmc, |
528 | ) |
529 | + services.resource_pools = ResourcePoolsService(connection=connection) |
530 | return services |
531 | diff --git a/src/maasapiserver/v3/services/resource_pools.py b/src/maasapiserver/v3/services/resource_pools.py |
532 | new file mode 100644 |
533 | index 0000000..6862bdc |
534 | --- /dev/null |
535 | +++ b/src/maasapiserver/v3/services/resource_pools.py |
536 | @@ -0,0 +1,65 @@ |
537 | +from typing import Optional |
538 | + |
539 | +from sqlalchemy.ext.asyncio import AsyncConnection |
540 | + |
541 | +from maasapiserver.common.models.constants import ( |
542 | + UNEXISTING_RESOURCE_VIOLATION_TYPE, |
543 | +) |
544 | +from maasapiserver.common.models.exceptions import ( |
545 | + BaseExceptionDetail, |
546 | + NotFoundException, |
547 | +) |
548 | +from maasapiserver.common.services._base import Service |
549 | +from maasapiserver.v3.api.models.requests.query import PaginationParams |
550 | +from maasapiserver.v3.api.models.requests.resource_pools import ( |
551 | + ResourcePoolPatchRequest, |
552 | + ResourcePoolRequest, |
553 | +) |
554 | +from maasapiserver.v3.db.resource_pools import ResourcePoolRepository |
555 | +from maasapiserver.v3.models.base import ListResult |
556 | +from maasapiserver.v3.models.resource_pools import ResourcePool |
557 | + |
558 | + |
559 | +class ResourcePoolsService(Service): |
560 | + def __init__( |
561 | + self, |
562 | + connection: AsyncConnection, |
563 | + resource_pools_repository: Optional[ResourcePoolRepository] = None, |
564 | + ): |
565 | + super().__init__(connection) |
566 | + self.resource_pools_repository = ( |
567 | + resource_pools_repository or ResourcePoolRepository(connection) |
568 | + ) |
569 | + |
570 | + async def create( |
571 | + self, resource_pool_request: ResourcePoolRequest |
572 | + ) -> ResourcePool: |
573 | + return await self.resource_pools_repository.create( |
574 | + resource_pool_request |
575 | + ) |
576 | + |
577 | + async def get_by_id(self, id: int) -> Optional[ResourcePool]: |
578 | + return await self.resource_pools_repository.find_by_id(id) |
579 | + |
580 | + async def list( |
581 | + self, pagination_params: PaginationParams |
582 | + ) -> ListResult[ResourcePool]: |
583 | + return await self.resource_pools_repository.list(pagination_params) |
584 | + |
585 | + async def patch( |
586 | + self, id: int, patch_request: ResourcePoolPatchRequest |
587 | + ) -> ResourcePool: |
588 | + resource_pool = await self.get_by_id(id) |
589 | + if not resource_pool: |
590 | + raise NotFoundException( |
591 | + details=[ |
592 | + BaseExceptionDetail( |
593 | + type=UNEXISTING_RESOURCE_VIOLATION_TYPE, |
594 | + message=f"Resource pool with id '{id}' does not exist.", |
595 | + ) |
596 | + ] |
597 | + ) |
598 | + resource_pool = resource_pool.copy( |
599 | + update=patch_request.dict(exclude_none=True) |
600 | + ) |
601 | + return await self.resource_pools_repository.update(resource_pool) |
602 | diff --git a/src/tests/fixtures/factories/resource_pools.py b/src/tests/fixtures/factories/resource_pools.py |
603 | new file mode 100644 |
604 | index 0000000..036f453 |
605 | --- /dev/null |
606 | +++ b/src/tests/fixtures/factories/resource_pools.py |
607 | @@ -0,0 +1,49 @@ |
608 | +# Factories for the ResourcePool |
609 | +from datetime import datetime |
610 | +from typing import Any |
611 | + |
612 | +from maasapiserver.v3.models.resource_pools import ResourcePool |
613 | +from tests.maasapiserver.fixtures.db import Fixture |
614 | + |
615 | + |
616 | +async def create_test_resource_pool( |
617 | + fixture: Fixture, **extra_details: Any |
618 | +) -> ResourcePool: |
619 | + created_at = datetime.utcnow().astimezone() |
620 | + updated_at = datetime.utcnow().astimezone() |
621 | + resource_pools = { |
622 | + "name": "my_resource_pool", |
623 | + "description": "my_description", |
624 | + "created": created_at, |
625 | + "updated": updated_at, |
626 | + } |
627 | + resource_pools.update(extra_details) |
628 | + [created_resource_pools] = await fixture.create( |
629 | + "maasserver_resourcepool", |
630 | + [resource_pools], |
631 | + ) |
632 | + return ResourcePool(**created_resource_pools) |
633 | + |
634 | + |
635 | +async def create_n_test_resource_pools( |
636 | + fixture: Fixture, size: int |
637 | +) -> list[ResourcePool]: |
638 | + now = datetime.utcnow().astimezone() |
639 | + resource_pools = { |
640 | + "name": "my_resource_pool", |
641 | + "description": "my_description", |
642 | + "created": now, |
643 | + "updated": now, |
644 | + } |
645 | + |
646 | + all_pools = [ |
647 | + resource_pools | {"name": str(i), "description": str(i)} |
648 | + for i in range(size) |
649 | + ] |
650 | + created_resource_pools = await fixture.create( |
651 | + "maasserver_resourcepool", |
652 | + all_pools, |
653 | + ) |
654 | + return [ |
655 | + ResourcePool(**created_pool) for created_pool in created_resource_pools |
656 | + ] |
657 | diff --git a/src/tests/maasapiserver/v3/api/models/requests/test_base.py b/src/tests/maasapiserver/v3/api/models/requests/test_base.py |
658 | index 5c86ec1..d87f29e 100644 |
659 | --- a/src/tests/maasapiserver/v3/api/models/requests/test_base.py |
660 | +++ b/src/tests/maasapiserver/v3/api/models/requests/test_base.py |
661 | @@ -1,31 +1,59 @@ |
662 | import pytest |
663 | |
664 | -from maasapiserver.v3.api.models.requests.base import NamedBaseModel |
665 | +from maasapiserver.v3.api.models.requests.base import ( |
666 | + NamedBaseModel, |
667 | + OptionalNamedBaseModel, |
668 | +) |
669 | + |
670 | +VALID_NAMES = [ |
671 | + "ValidName", |
672 | + "Name With Spaces", |
673 | + "Name-With-Hyphens", |
674 | + "123ValidName", |
675 | + "name with trailing hyphens-", |
676 | +] |
677 | + |
678 | +INVALID_NAMES = [ |
679 | + "Name_With_Special#Characters", |
680 | + "", |
681 | + " ", |
682 | + "-Name with leading hyphens", |
683 | +] |
684 | |
685 | |
686 | class TestNamedBaseModel: |
687 | @pytest.mark.parametrize( |
688 | "name", |
689 | - [ |
690 | - "ValidName", |
691 | - "Name With Spaces", |
692 | - "Name-With-Hyphens", |
693 | - "123ValidName", |
694 | - "name with trailing hyphens-", |
695 | - ], |
696 | + VALID_NAMES, |
697 | ) |
698 | def test_valid_names(self, name: str): |
699 | assert NamedBaseModel(name=name).name == name |
700 | |
701 | @pytest.mark.parametrize( |
702 | "name", |
703 | - [ |
704 | - "Name_With_Special#Characters", |
705 | - "", |
706 | - " ", |
707 | - "-Name with leading hyphens", |
708 | - ], |
709 | + INVALID_NAMES, |
710 | ) |
711 | def test_invalid_names(self, name: str): |
712 | with pytest.raises(ValueError, match="Invalid entity name."): |
713 | NamedBaseModel(name=name) |
714 | + |
715 | + |
716 | +class TestOptionalNamedBaseModel: |
717 | + @pytest.mark.parametrize("name", VALID_NAMES) |
718 | + def test_valid_names(self, name: str): |
719 | + assert OptionalNamedBaseModel(name=name).name == name |
720 | + |
721 | + @pytest.mark.parametrize("name", INVALID_NAMES) |
722 | + def test_invalid_names(self, name: str): |
723 | + with pytest.raises(ValueError, match="Invalid entity name."): |
724 | + OptionalNamedBaseModel(name=name) |
725 | + |
726 | + def test_none_name(self): |
727 | + model = OptionalNamedBaseModel() |
728 | + assert model.name is None |
729 | + |
730 | + def test_none_name_if_set(self): |
731 | + with pytest.raises( |
732 | + ValueError, match="The name for the resource must not be null." |
733 | + ): |
734 | + OptionalNamedBaseModel(name=None) |
735 | diff --git a/src/tests/maasapiserver/v3/api/test_resource_pools.py b/src/tests/maasapiserver/v3/api/test_resource_pools.py |
736 | new file mode 100644 |
737 | index 0000000..4107c1e |
738 | --- /dev/null |
739 | +++ b/src/tests/maasapiserver/v3/api/test_resource_pools.py |
740 | @@ -0,0 +1,275 @@ |
741 | +from datetime import timezone |
742 | + |
743 | +from fastapi.encoders import jsonable_encoder |
744 | +from httpx import AsyncClient |
745 | +import pytest |
746 | + |
747 | +from maasapiserver.common.api.models.responses.errors import ErrorBodyResponse |
748 | +from maasapiserver.v3.api.models.requests.resource_pools import ( |
749 | + ResourcePoolPatchRequest, |
750 | + ResourcePoolRequest, |
751 | +) |
752 | +from maasapiserver.v3.api.models.responses.resource_pools import ( |
753 | + ResourcePoolResponse, |
754 | + ResourcePoolsListResponse, |
755 | +) |
756 | +from maasapiserver.v3.constants import EXTERNAL_V3_API_PREFIX |
757 | +from maasapiserver.v3.models.resource_pools import ResourcePool |
758 | +from tests.fixtures.factories.resource_pools import ( |
759 | + create_n_test_resource_pools, |
760 | + create_test_resource_pool, |
761 | +) |
762 | +from tests.maasapiserver.fixtures.db import Fixture |
763 | + |
764 | + |
765 | +@pytest.mark.usefixtures("ensuremaasdb") |
766 | +@pytest.mark.asyncio |
767 | +class TestResourcePoolApi: |
768 | + def _assert_resource_pools_in_list( |
769 | + self, |
770 | + resource_pools: ResourcePool, |
771 | + resource_pools_response: ResourcePoolsListResponse, |
772 | + ) -> None: |
773 | + resource_pools_response = next( |
774 | + filter( |
775 | + lambda resource_pools_response: resource_pools.id |
776 | + == resource_pools_response.id, |
777 | + resource_pools_response.items, |
778 | + ) |
779 | + ) |
780 | + assert resource_pools.id == resource_pools_response.id |
781 | + assert resource_pools.name == resource_pools_response.name |
782 | + assert ( |
783 | + resource_pools.description == resource_pools_response.description |
784 | + ) |
785 | + |
786 | + @pytest.mark.parametrize("resource_pools_size", range(1, 3)) |
787 | + async def test_list( |
788 | + self, |
789 | + resource_pools_size: int, |
790 | + api_client: AsyncClient, |
791 | + fixture: Fixture, |
792 | + ) -> None: |
793 | + created_resource_pools = await create_n_test_resource_pools( |
794 | + fixture, size=resource_pools_size |
795 | + ) |
796 | + response = await api_client.get("/api/v3/resource_pools") |
797 | + assert response.status_code == 200 |
798 | + |
799 | + resource_pools_response = ResourcePoolsListResponse(**response.json()) |
800 | + assert resource_pools_response.kind == "ResourcePoolList" |
801 | + # Increment as the default resource pool is included in the count. |
802 | + assert resource_pools_response.total == resource_pools_size + 1 |
803 | + assert len(resource_pools_response.items) == resource_pools_size + 1 |
804 | + for resource_pools in created_resource_pools: |
805 | + self._assert_resource_pools_in_list( |
806 | + resource_pools, resource_pools_response |
807 | + ) |
808 | + |
809 | + async def test_parametrised_list( |
810 | + self, api_client: AsyncClient, fixture: Fixture |
811 | + ) -> None: |
812 | + await create_n_test_resource_pools(fixture, size=9) |
813 | + |
814 | + for page in range(1, 6): |
815 | + response = await api_client.get( |
816 | + f"/api/v3/resource_pools?page={page}&size=2" |
817 | + ) |
818 | + assert response.status_code == 200 |
819 | + resource_pools_response = ResourcePoolsListResponse( |
820 | + **response.json() |
821 | + ) |
822 | + assert resource_pools_response.kind == "ResourcePoolList" |
823 | + assert resource_pools_response.total == 10 |
824 | + assert len(resource_pools_response.items) == 2 |
825 | + |
826 | + @pytest.mark.parametrize( |
827 | + "page,size", [(1, 0), (0, 1), (-1, -1), (1, 1001)] |
828 | + ) |
829 | + async def test_invalid_list( |
830 | + self, page: int, size: int, api_client: AsyncClient |
831 | + ) -> None: |
832 | + response = await api_client.get( |
833 | + f"/api/v3/resource_pools?page={page}&size={size}" |
834 | + ) |
835 | + assert response.status_code == 422 |
836 | + |
837 | + error_response = ErrorBodyResponse(**response.json()) |
838 | + assert error_response.kind == "Error" |
839 | + assert error_response.code == 422 |
840 | + |
841 | + async def test_get( |
842 | + self, api_client: AsyncClient, fixture: Fixture |
843 | + ) -> None: |
844 | + created_resource_pools = await create_test_resource_pool(fixture) |
845 | + response = await api_client.get( |
846 | + f"/api/v3/resource_pools/{created_resource_pools.id}" |
847 | + ) |
848 | + assert response.status_code == 200 |
849 | + assert len(response.headers["ETag"]) > 0 |
850 | + assert response.json() == { |
851 | + "kind": "ResourcePool", |
852 | + "id": created_resource_pools.id, |
853 | + "name": created_resource_pools.name, |
854 | + "description": created_resource_pools.description, |
855 | + "created": created_resource_pools.created.isoformat(), |
856 | + "updated": created_resource_pools.updated.isoformat(), |
857 | + "_embedded": None, |
858 | + "_links": { |
859 | + "self": { |
860 | + "href": f"{EXTERNAL_V3_API_PREFIX}/resource_pools/{created_resource_pools.id}" |
861 | + } |
862 | + }, |
863 | + } |
864 | + |
865 | + @pytest.mark.parametrize("id,error", [("100", 404), ("xyz", 422)]) |
866 | + async def test_get_invalid( |
867 | + self, api_client: AsyncClient, id: str, error: int |
868 | + ) -> None: |
869 | + response = await api_client.get(f"/api/v3/resource_pools/{id}") |
870 | + assert response.status_code == error |
871 | + assert "ETag" not in response.headers |
872 | + |
873 | + error_response = ErrorBodyResponse(**response.json()) |
874 | + assert error_response.kind == "Error" |
875 | + assert error_response.code == error |
876 | + |
877 | + async def test_create(self, api_client: AsyncClient) -> None: |
878 | + resource_pool_request = ResourcePoolRequest( |
879 | + name="new_resource pool", description="new_pool_description" |
880 | + ) |
881 | + response = await api_client.post( |
882 | + "/api/v3/resource_pools", |
883 | + json=jsonable_encoder(resource_pool_request), |
884 | + ) |
885 | + assert response.status_code == 201 |
886 | + assert len(response.headers["ETag"]) > 0 |
887 | + resource_pools_response = ResourcePoolResponse(**response.json()) |
888 | + assert resource_pools_response.id > 1 |
889 | + assert resource_pools_response.name == resource_pool_request.name |
890 | + assert ( |
891 | + resource_pools_response.description |
892 | + == resource_pool_request.description |
893 | + ) |
894 | + assert ( |
895 | + resource_pools_response.hal_links.self.href |
896 | + == f"{EXTERNAL_V3_API_PREFIX}/resource_pools/{resource_pools_response.id}" |
897 | + ) |
898 | + |
899 | + @pytest.mark.parametrize( |
900 | + "error_code,request_data", |
901 | + [ |
902 | + (422, {"name": None}), |
903 | + (422, {"description": None}), |
904 | + (422, {"name": "", "description": "test"}), |
905 | + (422, {"name": "-my_pool", "description": "test"}), |
906 | + (422, {"name": "my$pool", "description": "test"}), |
907 | + ], |
908 | + ) |
909 | + async def test_create_invalid( |
910 | + self, |
911 | + api_client: AsyncClient, |
912 | + error_code: int, |
913 | + request_data: dict[str, str], |
914 | + ) -> None: |
915 | + response = await api_client.post( |
916 | + "/api/v3/resource_pools", json=request_data |
917 | + ) |
918 | + assert response.status_code == error_code |
919 | + |
920 | + error_response = ErrorBodyResponse(**response.json()) |
921 | + assert error_response.kind == "Error" |
922 | + assert error_response.code == error_code |
923 | + |
924 | + async def test_patch( |
925 | + self, |
926 | + api_client: AsyncClient, |
927 | + fixture: Fixture, |
928 | + ) -> None: |
929 | + resource_pool = await create_test_resource_pool(fixture=fixture) |
930 | + patch_resource_pool_request = ResourcePoolPatchRequest( |
931 | + name="newname", description="new description" |
932 | + ) |
933 | + response = await api_client.patch( |
934 | + f"/api/v3/resource_pools/{resource_pool.id}", |
935 | + json=jsonable_encoder(patch_resource_pool_request), |
936 | + ) |
937 | + assert response.status_code == 200 |
938 | + |
939 | + patch_resource_pool = ResourcePoolResponse(**response.json()) |
940 | + assert patch_resource_pool.id == resource_pool.id |
941 | + assert patch_resource_pool.name == patch_resource_pool_request.name |
942 | + assert ( |
943 | + patch_resource_pool.description |
944 | + == patch_resource_pool_request.description |
945 | + ) |
946 | + assert patch_resource_pool.created.astimezone( |
947 | + timezone.utc |
948 | + ) == resource_pool.created.astimezone(timezone.utc) |
949 | + assert patch_resource_pool.updated.astimezone( |
950 | + timezone.utc |
951 | + ) >= resource_pool.updated.astimezone(timezone.utc) |
952 | + |
953 | + patch_resource_pool_request2 = ResourcePoolPatchRequest( |
954 | + description="new description" |
955 | + ) |
956 | + response = await api_client.patch( |
957 | + f"/api/v3/resource_pools/{resource_pool.id}", |
958 | + json=jsonable_encoder( |
959 | + patch_resource_pool_request2, exclude_none=True |
960 | + ), |
961 | + ) |
962 | + assert response.status_code == 200 |
963 | + |
964 | + patch_resource_pool2 = ResourcePoolResponse(**response.json()) |
965 | + assert patch_resource_pool2.id == resource_pool.id |
966 | + assert patch_resource_pool2.name == patch_resource_pool_request.name |
967 | + assert ( |
968 | + patch_resource_pool2.description |
969 | + == patch_resource_pool_request2.description |
970 | + ) |
971 | + assert patch_resource_pool2.created.astimezone( |
972 | + timezone.utc |
973 | + ) == patch_resource_pool.created.astimezone(timezone.utc) |
974 | + assert patch_resource_pool2.updated.astimezone( |
975 | + timezone.utc |
976 | + ) >= patch_resource_pool.updated.astimezone(timezone.utc) |
977 | + |
978 | + async def test_patch_unexisting( |
979 | + self, |
980 | + api_client: AsyncClient, |
981 | + fixture: Fixture, |
982 | + ) -> None: |
983 | + patch_resource_pool_request = ResourcePoolPatchRequest( |
984 | + name="newname", description="new description" |
985 | + ) |
986 | + response = await api_client.patch( |
987 | + "/api/v3/resource_pools/1000", |
988 | + json=jsonable_encoder(patch_resource_pool_request), |
989 | + ) |
990 | + assert response.status_code == 404 |
991 | + error_response = ErrorBodyResponse(**response.json()) |
992 | + assert error_response.code == 404 |
993 | + |
994 | + @pytest.mark.parametrize( |
995 | + "error_code,request_data", |
996 | + [ |
997 | + (422, {"name": None}), |
998 | + (422, {"description": None}), |
999 | + (422, {"name": "", "description": "test"}), |
1000 | + (422, {"name": None, "description": "test"}), |
1001 | + (422, {"name": "-my_pool", "description": "test"}), |
1002 | + (422, {"name": "my$pool", "description": "test"}), |
1003 | + ], |
1004 | + ) |
1005 | + async def test_patch_invalid( |
1006 | + self, |
1007 | + api_client: AsyncClient, |
1008 | + fixture: Fixture, |
1009 | + error_code: int, |
1010 | + request_data: dict[str, str], |
1011 | + ) -> None: |
1012 | + response = await api_client.patch( |
1013 | + "/api/v3/resource_pools/0", json=request_data |
1014 | + ) |
1015 | + assert response.status_code == error_code |
1016 | diff --git a/src/tests/maasapiserver/v3/db/test_resource_pools.py b/src/tests/maasapiserver/v3/db/test_resource_pools.py |
1017 | new file mode 100644 |
1018 | index 0000000..6ad5feb |
1019 | --- /dev/null |
1020 | +++ b/src/tests/maasapiserver/v3/db/test_resource_pools.py |
1021 | @@ -0,0 +1,149 @@ |
1022 | +from datetime import datetime, timezone |
1023 | +from math import ceil |
1024 | + |
1025 | +import pytest |
1026 | +from sqlalchemy.ext.asyncio import AsyncConnection |
1027 | +from sqlalchemy.orm.exc import NoResultFound |
1028 | + |
1029 | +from maasapiserver.common.models.exceptions import AlreadyExistsException |
1030 | +from maasapiserver.v3.api.models.requests.query import PaginationParams |
1031 | +from maasapiserver.v3.api.models.requests.resource_pools import ( |
1032 | + ResourcePoolRequest, |
1033 | +) |
1034 | +from maasapiserver.v3.db.resource_pools import ResourcePoolRepository |
1035 | +from maasapiserver.v3.models.resource_pools import ResourcePool |
1036 | +from tests.fixtures.factories.resource_pools import ( |
1037 | + create_n_test_resource_pools, |
1038 | + create_test_resource_pool, |
1039 | +) |
1040 | +from tests.maasapiserver.fixtures.db import Fixture |
1041 | + |
1042 | + |
1043 | +@pytest.mark.usefixtures("ensuremaasdb") |
1044 | +@pytest.mark.asyncio |
1045 | +class TestResourcePoolRepository: |
1046 | + async def test_create(self, db_connection: AsyncConnection) -> None: |
1047 | + now = datetime.utcnow() |
1048 | + resource_pools_repository = ResourcePoolRepository(db_connection) |
1049 | + created_resource_pools = await resource_pools_repository.create( |
1050 | + ResourcePoolRequest( |
1051 | + name="my_resource_pool", description="my description" |
1052 | + ) |
1053 | + ) |
1054 | + assert created_resource_pools.id |
1055 | + assert created_resource_pools.name == "my_resource_pool" |
1056 | + assert created_resource_pools.description == "my description" |
1057 | + assert created_resource_pools.created.astimezone( |
1058 | + timezone.utc |
1059 | + ) >= now.astimezone(timezone.utc) |
1060 | + assert created_resource_pools.updated.astimezone( |
1061 | + timezone.utc |
1062 | + ) >= now.astimezone(timezone.utc) |
1063 | + |
1064 | + async def test_create_duplicated( |
1065 | + self, db_connection: AsyncConnection, fixture: Fixture |
1066 | + ) -> None: |
1067 | + resource_pools_repository = ResourcePoolRepository(db_connection) |
1068 | + created_resource_pools = await create_test_resource_pool(fixture) |
1069 | + |
1070 | + with pytest.raises(AlreadyExistsException): |
1071 | + await resource_pools_repository.create( |
1072 | + ResourcePoolRequest( |
1073 | + name=created_resource_pools.name, |
1074 | + description=created_resource_pools.description, |
1075 | + ) |
1076 | + ) |
1077 | + |
1078 | + async def test_find_by_id( |
1079 | + self, db_connection: AsyncConnection, fixture: Fixture |
1080 | + ) -> None: |
1081 | + resource_pools_repository = ResourcePoolRepository(db_connection) |
1082 | + created_resource_pools = await create_test_resource_pool(fixture) |
1083 | + |
1084 | + resource_pools = await resource_pools_repository.find_by_id( |
1085 | + created_resource_pools.id |
1086 | + ) |
1087 | + assert resource_pools.id == created_resource_pools.id |
1088 | + assert resource_pools.name == created_resource_pools.name |
1089 | + assert resource_pools.description == created_resource_pools.description |
1090 | + assert resource_pools.updated == created_resource_pools.updated |
1091 | + assert resource_pools.created == created_resource_pools.created |
1092 | + |
1093 | + resource_pools = await resource_pools_repository.find_by_id(1234) |
1094 | + assert resource_pools is None |
1095 | + |
1096 | + @pytest.mark.parametrize("page_size", range(1, 12)) |
1097 | + async def test_list( |
1098 | + self, page_size: int, db_connection: AsyncConnection, fixture: Fixture |
1099 | + ) -> None: |
1100 | + resource_pools_repository = ResourcePoolRepository(db_connection) |
1101 | + resource_pools_count = 10 |
1102 | + created_resource_pools = ( |
1103 | + await create_n_test_resource_pools( |
1104 | + fixture, size=resource_pools_count - 1 |
1105 | + ) |
1106 | + )[::-1] |
1107 | + total_pages = ceil(resource_pools_count / page_size) |
1108 | + for page in range(1, total_pages + 1): |
1109 | + resource_pools_result = await resource_pools_repository.list( |
1110 | + PaginationParams(size=page_size, page=page) |
1111 | + ) |
1112 | + assert resource_pools_result.total == resource_pools_count |
1113 | + assert total_pages == ceil(resource_pools_result.total / page_size) |
1114 | + if page == total_pages: # last page may have fewer elements |
1115 | + assert len(resource_pools_result.items) == ( |
1116 | + page_size |
1117 | + - ((total_pages * page_size) % resource_pools_result.total) |
1118 | + ) |
1119 | + else: |
1120 | + assert len(resource_pools_result.items) == page_size |
1121 | + for resource_pools in created_resource_pools[ |
1122 | + ((page - 1) * page_size) : ((page * page_size)) |
1123 | + ]: |
1124 | + assert resource_pools in resource_pools_result.items |
1125 | + |
1126 | + async def test_update( |
1127 | + self, db_connection: AsyncConnection, fixture: Fixture |
1128 | + ) -> None: |
1129 | + resource_pools_repository = ResourcePoolRepository(db_connection) |
1130 | + created_resource_pool = await create_test_resource_pool(fixture) |
1131 | + updated_request = created_resource_pool.copy() |
1132 | + updated_request.name = "new name" |
1133 | + updated_request.description = "new description" |
1134 | + updated_pools = await resource_pools_repository.update(updated_request) |
1135 | + # unchanged |
1136 | + assert updated_pools.id == created_resource_pool.id |
1137 | + assert updated_pools.created == created_resource_pool.created |
1138 | + # changed |
1139 | + assert updated_pools.name == "new name" |
1140 | + assert updated_pools.description == "new description" |
1141 | + assert updated_pools.updated > created_resource_pool.updated |
1142 | + |
1143 | + async def test_update_duplicated_name( |
1144 | + self, db_connection: AsyncConnection, fixture: Fixture |
1145 | + ) -> None: |
1146 | + resource_pools_repository = ResourcePoolRepository(db_connection) |
1147 | + created_resource_pool = await create_test_resource_pool( |
1148 | + fixture, name="test1" |
1149 | + ) |
1150 | + created_resource_pool2 = await create_test_resource_pool( |
1151 | + fixture, name="test2" |
1152 | + ) |
1153 | + |
1154 | + updated_resource_pool = created_resource_pool.copy( |
1155 | + update={"id": created_resource_pool2.id} |
1156 | + ) |
1157 | + with pytest.raises(AlreadyExistsException): |
1158 | + await resource_pools_repository.update(updated_resource_pool) |
1159 | + |
1160 | + async def test_update_nonexistent( |
1161 | + self, db_connection: AsyncConnection |
1162 | + ) -> None: |
1163 | + now = datetime.utcnow() |
1164 | + resource_pools_repository = ResourcePoolRepository(db_connection) |
1165 | + resource_pool = ResourcePool( |
1166 | + id=1000, name="test", description="test", created=now, updated=now |
1167 | + ) |
1168 | + |
1169 | + with pytest.raises(NoResultFound): |
1170 | + await resource_pools_repository.update(resource_pool) |
1171 | diff --git a/src/tests/maasapiserver/v3/models/test_resource_pools.py b/src/tests/maasapiserver/v3/models/test_resource_pools.py |
1172 | new file mode 100644 |
1173 | index 0000000..398ac93 |
1174 | --- /dev/null |
1175 | +++ b/src/tests/maasapiserver/v3/models/test_resource_pools.py |
1176 | @@ -0,0 +1,36 @@ |
1177 | +import datetime |
1178 | + |
1179 | +from maasapiserver.v3.models.resource_pools import ResourcePool |
1180 | + |
1181 | + |
1182 | +class TestResourcePoolsModel: |
1183 | + def test_to_response(self) -> None: |
1184 | + now = datetime.datetime.utcnow() |
1185 | + resource_pools = ResourcePool( |
1186 | + id=1, |
1187 | + name="my resource_pools", |
1188 | + description="my description", |
1189 | + created=now, |
1190 | + updated=now, |
1191 | + ) |
1192 | + |
1193 | + response = resource_pools.to_response("/api/v3/") |
1194 | + assert resource_pools.id == response.id |
1195 | + assert resource_pools.name == response.name |
1196 | + assert resource_pools.description == response.description |
1197 | + assert response.hal_links.self.href == "/api/v3/1" |
1198 | + |
1199 | + def test_etag(self) -> None: |
1200 | + now = datetime.datetime.fromtimestamp(1705671128) |
1201 | + resource_pools = ResourcePool( |
1202 | + id=1, |
1203 | + name="my resource pools", |
1204 | + description="my description", |
1205 | + created=now, |
1206 | + updated=now, |
1207 | + ) |
1208 | + |
1209 | + assert ( |
1210 | + resource_pools.etag() |
1211 | + == "979626792c99c0860c39341ea26ae63a1ef7ca922d156b969777c05db3bee295" |
1212 | + ) |
1213 | diff --git a/src/tests/maasapiserver/v3/services/test_resource_pools.py b/src/tests/maasapiserver/v3/services/test_resource_pools.py |
1214 | new file mode 100644 |
1215 | index 0000000..b3012a3 |
1216 | --- /dev/null |
1217 | +++ b/src/tests/maasapiserver/v3/services/test_resource_pools.py |
1218 | @@ -0,0 +1,151 @@ |
1219 | +from datetime import datetime |
1220 | +from unittest.mock import AsyncMock, Mock |
1221 | + |
1222 | +import pytest |
1223 | +from sqlalchemy.ext.asyncio import AsyncConnection |
1224 | + |
1225 | +from maasapiserver.common.models.exceptions import NotFoundException |
1226 | +from maasapiserver.v3.api.models.requests.query import PaginationParams |
1227 | +from maasapiserver.v3.api.models.requests.resource_pools import ( |
1228 | + ResourcePoolPatchRequest, |
1229 | + ResourcePoolRequest, |
1230 | +) |
1231 | +from maasapiserver.v3.db.resource_pools import ResourcePoolRepository |
1232 | +from maasapiserver.v3.models.base import ListResult |
1233 | +from maasapiserver.v3.models.resource_pools import ResourcePool |
1234 | +from maasapiserver.v3.services import ResourcePoolsService |
1235 | +from tests.maasapiserver.fixtures.db import Fixture |
1236 | + |
1237 | + |
1238 | +@pytest.mark.usefixtures("ensuremaasdb") |
1239 | +@pytest.mark.asyncio |
1240 | +class TestResourcePoolsService: |
1241 | + async def test_create( |
1242 | + self, db_connection: AsyncConnection, fixture: Fixture |
1243 | + ) -> None: |
1244 | + now = datetime.utcnow() |
1245 | + resource_pool = ResourcePool( |
1246 | + id=1, |
1247 | + name="test", |
1248 | + description="description", |
1249 | + created=now, |
1250 | + updated=now, |
1251 | + ) |
1252 | + resource_pool_repository_mock = Mock(ResourcePoolRepository) |
1253 | + resource_pool_repository_mock.create = AsyncMock( |
1254 | + return_value=resource_pool |
1255 | + ) |
1256 | + resource_pools_service = ResourcePoolsService( |
1257 | + connection=db_connection, |
1258 | + resource_pools_repository=resource_pool_repository_mock, |
1259 | + ) |
1260 | + request = ResourcePoolRequest( |
1261 | + name=resource_pool.name, description=resource_pool.description |
1262 | + ) |
1263 | + created_resource_pool = await resource_pools_service.create(request) |
1264 | + resource_pool_repository_mock.create.assert_called_once_with(request) |
1265 | + assert created_resource_pool is not None |
1266 | + |
1267 | + async def test_list( |
1268 | + self, db_connection: AsyncConnection, fixture: Fixture |
1269 | + ) -> None: |
1270 | + resource_pool_repository_mock = Mock(ResourcePoolRepository) |
1271 | + resource_pool_repository_mock.list = AsyncMock( |
1272 | + return_value=ListResult[ResourcePool](items=[], total=0) |
1273 | + ) |
1274 | + resource_pools_service = ResourcePoolsService( |
1275 | + connection=db_connection, |
1276 | + resource_pools_repository=resource_pool_repository_mock, |
1277 | + ) |
1278 | + pagination_params = PaginationParams(page=1, size=1) |
1279 | + resource_pools_list = await resource_pools_service.list( |
1280 | + pagination_params |
1281 | + ) |
1282 | + resource_pool_repository_mock.list.assert_called_once_with( |
1283 | + pagination_params |
1284 | + ) |
1285 | + assert resource_pools_list.total == 0 |
1286 | + assert resource_pools_list.items == [] |
1287 | + |
1288 | + async def test_get_by_id( |
1289 | + self, db_connection: AsyncConnection, fixture: Fixture |
1290 | + ) -> None: |
1291 | + now = datetime.utcnow() |
1292 | + resource_pool = ResourcePool( |
1293 | + id=1, |
1294 | + name="test", |
1295 | + description="description", |
1296 | + created=now, |
1297 | + updated=now, |
1298 | + ) |
1299 | + resource_pool_repository_mock = Mock(ResourcePoolRepository) |
1300 | + resource_pool_repository_mock.find_by_id = AsyncMock( |
1301 | + return_value=resource_pool |
1302 | + ) |
1303 | + |
1304 | + resource_pools_service = ResourcePoolsService( |
1305 | + connection=db_connection, |
1306 | + resource_pools_repository=resource_pool_repository_mock, |
1307 | + ) |
1308 | + retrieved_resource_pool = await resource_pools_service.get_by_id( |
1309 | + id=resource_pool.id |
1310 | + ) |
1311 | + resource_pool_repository_mock.find_by_id.assert_called_once_with( |
1312 | + resource_pool.id |
1313 | + ) |
1314 | + assert retrieved_resource_pool == resource_pool |
1315 | + |
1316 | + async def test_patch_not_found( |
1317 | + self, db_connection: AsyncConnection, fixture: Fixture |
1318 | + ) -> None: |
1319 | + resource_pool_repository_mock = Mock(ResourcePoolRepository) |
1320 | + resource_pool_repository_mock.find_by_id = AsyncMock(return_value=None) |
1321 | + resource_pools_service = ResourcePoolsService( |
1322 | + connection=db_connection, |
1323 | + resource_pools_repository=resource_pool_repository_mock, |
1324 | + ) |
1325 | + with pytest.raises(NotFoundException): |
1326 | + await resource_pools_service.patch( |
1327 | + id=1000, |
1328 | + patch_request=ResourcePoolPatchRequest( |
1329 | + name="name", description="description" |
1330 | + ), |
1331 | + ) |
1332 | + |
1333 | + async def test_patch( |
1334 | + self, db_connection: AsyncConnection, fixture: Fixture |
1335 | + ) -> None: |
1336 | + now = datetime.utcnow() |
1337 | + resource_pool = ResourcePool( |
1338 | + id=1, |
1339 | + name="test", |
1340 | + description="description", |
1341 | + created=now, |
1342 | + updated=now, |
1343 | + ) |
1344 | + patch_resource_pool = resource_pool.copy( |
1345 | + update={"name": "test2", "description": "description2"} |
1346 | + ) |
1347 | + resource_pool_repository_mock = Mock(ResourcePoolRepository) |
1348 | + resource_pool_repository_mock.find_by_id = AsyncMock( |
1349 | + return_value=resource_pool |
1350 | + ) |
1351 | + resource_pool_repository_mock.update = AsyncMock( |
1352 | + return_value=patch_resource_pool |
1353 | + ) |
1354 | + |
1355 | + resource_pools_service = ResourcePoolsService( |
1356 | + connection=db_connection, |
1357 | + resource_pools_repository=resource_pool_repository_mock, |
1358 | + ) |
1359 | + updated_resource_pool = await resource_pools_service.patch( |
1360 | + id=resource_pool.id, |
1361 | + patch_request=ResourcePoolPatchRequest( |
1362 | + name=patch_resource_pool.name, |
1363 | + description=patch_resource_pool.description, |
1364 | + ), |
1365 | + ) |
1366 | + resource_pool_repository_mock.update.assert_called_once_with( |
1367 | + patch_resource_pool |
1368 | + ) |
1369 | + assert updated_resource_pool == patch_resource_pool |
UNIT TESTS
-b v3-resourcepool lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED maas-ci. internal: 8080/job/ maas-tester/ 4762/console 5099fe215f268cd 34b450bfdd
LOG: http://
COMMIT: d54f8b6cb5bf09a