Skip to content

Commit bdf64c1

Browse files
Implement and test update endpoint
1 parent 13a4d1d commit bdf64c1

File tree

3 files changed

+285
-4
lines changed

3 files changed

+285
-4
lines changed

ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,17 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do
5353
end
5454
end
5555

56-
def update(_request, _stream) do
57-
%UpdateResponse{}
56+
def update(request, _stream) do
57+
case EphemeralEnvironments.Service.EphemeralEnvironmentType.update(request.environment_type) do
58+
{:ok, environment_type} ->
59+
%{environment_type: environment_type}
60+
61+
{:error, :not_found} ->
62+
raise GRPC.RPCError, status: :not_found, message: "Environment type not found"
63+
64+
{:error, error_message} ->
65+
raise GRPC.RPCError, status: :unknown, message: error_message
66+
end
5867
end
5968

6069
def delete(_request, _stream) do

ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,70 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do
4343
end
4444
end
4545

46+
@doc """
47+
Updates an existing ephemeral environment type.
48+
49+
## Parameters
50+
- attrs: Map with keys:
51+
- id (required)
52+
- org_id (required)
53+
- last_updated_by (required)
54+
- name (optional)
55+
- description (optional)
56+
- max_number_of_instances (optional)
57+
- state (optional)
58+
59+
## Returns
60+
- {:ok, map} on success
61+
- {:error, :not_found} if the environment type doesn't exist
62+
- {:error, String.t()} on validation failure
63+
"""
64+
def update(attrs) do
65+
# Filter out proto default values that shouldn't be updated
66+
attrs = filter_proto_defaults(attrs)
67+
68+
with {:ok, record} <- get_record(attrs[:id], attrs[:org_id]),
69+
{:ok, updated_record} <- update_record(record, attrs) do
70+
{:ok, struct_to_map(updated_record)}
71+
end
72+
end
73+
74+
# Remove proto default values that indicate "not set" rather than explicit values
75+
defp filter_proto_defaults(attrs) do
76+
attrs
77+
|> Enum.reject(fn
78+
# Empty strings from proto mean "not set"
79+
{_key, ""} -> true
80+
# :unspecified enum means "not set"
81+
{:state, :unspecified} -> true
82+
# 0 for max_number_of_instances means "not set" (since validation requires > 0)
83+
{:max_number_of_instances, 0} -> true
84+
# Keep everything else
85+
_ -> false
86+
end)
87+
|> Map.new()
88+
end
89+
90+
defp get_record(id, org_id) when is_binary(id) and is_binary(org_id) do
91+
Schema
92+
|> where([e], e.id == ^id and e.org_id == ^org_id)
93+
|> Repo.one()
94+
|> case do
95+
nil -> {:error, :not_found}
96+
record -> {:ok, record}
97+
end
98+
end
99+
100+
defp update_record(record, attrs) do
101+
record
102+
|> Schema.changeset(attrs)
103+
|> Repo.update()
104+
|> case do
105+
{:ok, updated_record} -> {:ok, updated_record}
106+
{:error, changeset} -> {:error, format_errors(changeset)}
107+
end
108+
end
109+
46110
@doc """
47111
Creates a new ephemeral environment type.
48112
@@ -92,12 +156,19 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do
92156
defp format_errors(changeset) do
93157
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
94158
Enum.reduce(opts, msg, fn {key, value}, acc ->
95-
String.replace(acc, "%{#{key}}", to_string(value))
159+
String.replace(acc, "%{#{key}}", safe_to_string(value))
96160
end)
97161
end)
98162
|> Enum.map(fn {field, errors} ->
99163
"#{field}: #{Enum.join(errors, ", ")}"
100164
end)
101165
|> Enum.join("; ")
102166
end
167+
168+
# Safely convert values to strings, handling complex types
169+
defp safe_to_string(value) when is_binary(value), do: value
170+
defp safe_to_string(value) when is_atom(value), do: to_string(value)
171+
defp safe_to_string(value) when is_number(value), do: to_string(value)
172+
defp safe_to_string(value) when is_list(value), do: inspect(value)
173+
defp safe_to_string(value), do: inspect(value)
103174
end

ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do
1010
EphemeralEnvironmentType,
1111
EphemeralEnvironments,
1212
ListRequest,
13-
DescribeRequest
13+
DescribeRequest,
14+
UpdateRequest
1415
}
1516

1617
@org_id Ecto.UUID.generate()
@@ -199,6 +200,206 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do
199200
end
200201

201202
describe "update/2" do
203+
test "updates environment type successfully", %{channel: channel} do
204+
{:ok, env_type} =
205+
Factories.EphemeralEnvironmentsType.insert(
206+
org_id: @org_id,
207+
name: "Original Name",
208+
description: "Original description",
209+
created_by: @user_id,
210+
state: :draft,
211+
max_number_of_instances: 5
212+
)
213+
214+
updater_id = Ecto.UUID.generate()
215+
216+
request = %UpdateRequest{
217+
environment_type: %EphemeralEnvironmentType{
218+
id: env_type.id,
219+
org_id: @org_id,
220+
name: "Updated Name",
221+
description: "Updated description",
222+
last_updated_by: updater_id,
223+
state: :TYPE_STATE_READY,
224+
max_number_of_instances: 10
225+
}
226+
}
227+
228+
{:ok, response} = EphemeralEnvironments.Stub.update(channel, request)
229+
230+
assert response.environment_type.id == env_type.id
231+
assert response.environment_type.org_id == @org_id
232+
assert response.environment_type.name == "Updated Name"
233+
assert response.environment_type.description == "Updated description"
234+
assert response.environment_type.last_updated_by == updater_id
235+
assert response.environment_type.state == :TYPE_STATE_READY
236+
assert response.environment_type.max_number_of_instances == 10
237+
# created_by should remain unchanged
238+
assert response.environment_type.created_by == @user_id
239+
240+
# Verify database record was updated
241+
db_record = Repo.get(Schema, env_type.id)
242+
assert db_record.name == "Updated Name"
243+
assert db_record.description == "Updated description"
244+
assert db_record.last_updated_by == updater_id
245+
assert db_record.state == :ready
246+
end
247+
248+
test "updates only provided fields", %{channel: channel} do
249+
{:ok, env_type} =
250+
Factories.EphemeralEnvironmentsType.insert(
251+
org_id: @org_id,
252+
name: "Original Name",
253+
description: "Original description",
254+
created_by: @user_id,
255+
state: :draft,
256+
max_number_of_instances: 5
257+
)
258+
259+
updater_id = Ecto.UUID.generate()
260+
261+
# Only update name and last_updated_by
262+
request = %UpdateRequest{
263+
environment_type: %EphemeralEnvironmentType{
264+
id: env_type.id,
265+
org_id: @org_id,
266+
name: "New Name",
267+
last_updated_by: updater_id
268+
}
269+
}
270+
271+
{:ok, response} = EphemeralEnvironments.Stub.update(channel, request)
272+
273+
assert response.environment_type.name == "New Name"
274+
assert response.environment_type.last_updated_by == updater_id
275+
# Other fields should remain unchanged
276+
assert response.environment_type.description == "Original description"
277+
assert response.environment_type.state == :TYPE_STATE_DRAFT
278+
assert response.environment_type.max_number_of_instances == 5
279+
end
280+
281+
test "returns not_found when environment type doesn't exist", %{channel: channel} do
282+
non_existent_id = Ecto.UUID.generate()
283+
284+
request = %UpdateRequest{
285+
environment_type: %EphemeralEnvironmentType{
286+
id: non_existent_id,
287+
org_id: @org_id,
288+
name: "Updated Name",
289+
last_updated_by: @user_id
290+
}
291+
}
292+
293+
assert {:error, %GRPC.RPCError{status: 5, message: "Environment type not found"}} =
294+
EphemeralEnvironments.Stub.update(channel, request)
295+
end
296+
297+
test "returns not_found when updating with wrong org_id", %{channel: channel} do
298+
{:ok, env_type} =
299+
Factories.EphemeralEnvironmentsType.insert(
300+
org_id: @org_id,
301+
name: "Test Environment"
302+
)
303+
304+
different_org_id = Ecto.UUID.generate()
305+
306+
request = %UpdateRequest{
307+
environment_type: %EphemeralEnvironmentType{
308+
id: env_type.id,
309+
org_id: different_org_id,
310+
name: "Updated Name",
311+
last_updated_by: @user_id
312+
}
313+
}
314+
315+
assert {:error, %GRPC.RPCError{status: 5, message: "Environment type not found"}} =
316+
EphemeralEnvironments.Stub.update(channel, request)
317+
end
318+
319+
test "fails validation when updating with duplicate name in same org", %{channel: channel} do
320+
{:ok, env1} =
321+
Factories.EphemeralEnvironmentsType.insert(
322+
org_id: @org_id,
323+
name: "Environment 1"
324+
)
325+
326+
{:ok, env2} =
327+
Factories.EphemeralEnvironmentsType.insert(
328+
org_id: @org_id,
329+
name: "Environment 2"
330+
)
331+
332+
# Try to rename env2 to env1's name
333+
request = %UpdateRequest{
334+
environment_type: %EphemeralEnvironmentType{
335+
id: env2.id,
336+
org_id: @org_id,
337+
name: "Environment 1",
338+
last_updated_by: @user_id
339+
}
340+
}
341+
342+
{:error, error} = EphemeralEnvironments.Stub.update(channel, request)
343+
assert %GRPC.RPCError{} = error
344+
# UNKNOWN
345+
assert error.status == 2
346+
assert error.message == "duplicate_name: ephemeral environment name has already been taken"
347+
end
348+
349+
test "allows updating to same name in different org", %{channel: channel} do
350+
{:ok, env1} =
351+
Factories.EphemeralEnvironmentsType.insert(
352+
org_id: @org_id,
353+
name: "Shared Name"
354+
)
355+
356+
other_org_id = Ecto.UUID.generate()
357+
358+
{:ok, env2} =
359+
Factories.EphemeralEnvironmentsType.insert(
360+
org_id: other_org_id,
361+
name: "Original Name"
362+
)
363+
364+
# Update env2 to use the same name as env1 (but different org)
365+
request = %UpdateRequest{
366+
environment_type: %EphemeralEnvironmentType{
367+
id: env2.id,
368+
org_id: other_org_id,
369+
name: "Shared Name",
370+
last_updated_by: @user_id
371+
}
372+
}
373+
374+
assert {:ok, response} = EphemeralEnvironments.Stub.update(channel, request)
375+
assert response.environment_type.name == "Shared Name"
376+
end
377+
378+
test "updates timestamp when updating", %{channel: channel} do
379+
{:ok, env_type} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id)
380+
381+
# Wait a bit to ensure timestamp changes
382+
:timer.sleep(100)
383+
384+
request = %UpdateRequest{
385+
environment_type: %EphemeralEnvironmentType{
386+
id: env_type.id,
387+
org_id: @org_id,
388+
name: "Updated Name",
389+
last_updated_by: @user_id
390+
}
391+
}
392+
393+
{:ok, response} = EphemeralEnvironments.Stub.update(channel, request)
394+
395+
# created_at should be the original timestamp
396+
original_created_at = DateTime.from_naive!(env_type.inserted_at, "Etc/UTC")
397+
response_created_at = DateTime.from_unix!(response.environment_type.created_at.seconds)
398+
assert DateTime.diff(response_created_at, original_created_at, :second) == 0
399+
400+
# updated_at should be recent
401+
assert_recent_timestamp(DateTime.from_unix!(response.environment_type.updated_at.seconds))
402+
end
202403
end
203404

204405
describe "delete/2" do

0 commit comments

Comments
 (0)