Skip to content

Commit 1928521

Browse files
committed
Improve tasks controller
1 parent b0039f6 commit 1928521

File tree

4 files changed

+348
-16
lines changed

4 files changed

+348
-16
lines changed

services/app/apps/codebattle/lib/codebattle/task.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ defmodule Codebattle.Task do
133133
comment: Map.get(params, :comment),
134134
creator_id: params[:creator_id],
135135
description_en: params.description_en,
136-
description_ru: params.description_ru,
136+
description_ru: Map.get(params, :description_ru, ""),
137137
examples: params.examples,
138138
generator_lang: Map.get(params, :generator_lang, "js"),
139139
input_signature: params.input_signature,
@@ -143,7 +143,7 @@ defmodule Codebattle.Task do
143143
output_signature: params.output_signature,
144144
solution: Map.get(params, :solution, ""),
145145
state: params.state,
146-
tags: params.tags,
146+
tags: Map.get(params, :tags, []),
147147
updated_at: DateTime.utc_now(),
148148
visibility: params.visibility
149149
]

services/app/apps/codebattle/lib/codebattle_web/controllers/ext_api/task_controller.ex

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,78 @@ defmodule CodebattleWeb.ExtApi.TaskController do
66
plug(CodebattleWeb.Plugs.TokenAuth)
77

88
def create(conn, params) do
9-
task_params = Map.get(params, "task")
9+
payload = Map.get(params, "payload")
1010
origin = Map.get(params, "origin")
1111
visibility = Map.get(params, "visibility")
1212

13-
params =
14-
task_params
15-
|> Map.put("state", "active")
16-
|> Map.put("origin", origin)
17-
|> Map.put("visibility", visibility)
18-
|> Runner.AtomizedMap.atomize()
13+
with {:ok, gzipped_data} <- decode_base64(payload),
14+
{:ok, json_data} <- decompress_gzip(gzipped_data),
15+
{:ok, tasks_list} <- Jason.decode(json_data) do
16+
results =
17+
Enum.map(tasks_list, fn task_params ->
18+
params =
19+
task_params
20+
|> Map.put("state", "active")
21+
|> Map.put("origin", origin)
22+
|> Map.put("visibility", visibility)
23+
|> Runner.AtomizedMap.atomize()
1924

20-
case Codebattle.Task.changeset(%Codebattle.Task{}, params) do
21-
%{valid?: true} ->
22-
params |> Codebattle.Task.upsert!() |> dbg()
23-
send_resp(conn, 201, "")
25+
case Codebattle.Task.changeset(%Codebattle.Task{}, params) do
26+
%{valid?: true} ->
27+
Codebattle.Task.upsert!(params)
28+
{:ok, task_params}
2429

25-
%{valid?: false, errors: errors} ->
26-
errors = Map.new(errors, fn {k, {v, _}} -> {k, v} end)
30+
%{valid?: false, errors: errors} ->
31+
{:error, task_params, errors}
32+
end
33+
end)
2734

35+
errors =
36+
results
37+
|> Enum.filter(fn
38+
{:error, _, _} -> true
39+
_ -> false
40+
end)
41+
|> Enum.map(fn {:error, task, errors} ->
42+
%{
43+
task: task,
44+
errors: Map.new(errors, fn {k, {v, _}} -> {k, v} end)
45+
}
46+
end)
47+
48+
success_count =
49+
Enum.count(results, fn
50+
{:ok, _} -> true
51+
_ -> false
52+
end)
53+
54+
if Enum.empty?(errors) do
55+
conn
56+
|> put_status(:created)
57+
|> json(%{success: success_count})
58+
else
59+
conn
60+
|> put_status(:bad_request)
61+
|> json(%{success: success_count, errors: errors})
62+
end
63+
else
64+
{:error, _reason} ->
2865
conn
2966
|> put_status(:bad_request)
30-
|> json(%{errors: errors})
67+
|> json(%{errors: %{payload: "Invalid gzipped payload format"}})
68+
end
69+
end
70+
71+
defp decompress_gzip(data) do
72+
{:ok, :zlib.gunzip(data)}
73+
rescue
74+
_ -> {:error, :invalid_gzip}
75+
end
76+
77+
defp decode_base64(data) do
78+
case Base.decode64(data) do
79+
{:ok, decoded} -> {:ok, decoded}
80+
:error -> {:error, :invalid_base64}
3181
end
3282
end
3383
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
defmodule CodebattleWeb.ExtApi.TaskControllerTest do
2+
use CodebattleWeb.ConnCase, async: false
3+
4+
alias Codebattle.Repo
5+
alias Codebattle.Task
6+
7+
describe "create/2" do
8+
test "checks auth", %{conn: conn} do
9+
payload = create_gzipped_payload([%{name: "test_task"}])
10+
11+
assert conn
12+
|> post(Routes.ext_api_task_path(conn, :create, %{payload: payload}))
13+
|> json_response(401)
14+
end
15+
16+
test "creates tasks with valid gzipped payload", %{conn: conn} do
17+
tasks_data = [
18+
%{
19+
name: "sum_of_two",
20+
description_en: "Calculate sum of two numbers",
21+
description_ru: "Вычислить сумму двух чисел",
22+
level: "easy",
23+
asserts: [
24+
%{arguments: [1, 1], expected: 2},
25+
%{arguments: [2, 3], expected: 5}
26+
],
27+
input_signature: [
28+
%{argument_name: "a", type: %{name: "integer"}},
29+
%{argument_name: "b", type: %{name: "integer"}}
30+
],
31+
output_signature: %{type: %{name: "integer"}},
32+
examples: "sum(1, 1) -> 2"
33+
},
34+
%{
35+
name: "multiply_numbers",
36+
description_en: "Multiply two numbers",
37+
description_ru: "Умножить два числа",
38+
level: "elementary",
39+
asserts: [
40+
%{arguments: [2, 3], expected: 6},
41+
%{arguments: [4, 5], expected: 20}
42+
],
43+
input_signature: [
44+
%{argument_name: "x", type: %{name: "integer"}},
45+
%{argument_name: "y", type: %{name: "integer"}}
46+
],
47+
output_signature: %{type: %{name: "integer"}},
48+
examples: "multiply(2, 3) -> 6"
49+
}
50+
]
51+
52+
payload = create_gzipped_payload(tasks_data)
53+
54+
response =
55+
conn
56+
|> put_req_header("x-auth-key", "x-key")
57+
|> post(
58+
Routes.ext_api_task_path(conn, :create, %{
59+
payload: payload,
60+
origin: "github",
61+
visibility: "hidden"
62+
})
63+
)
64+
|> json_response(201)
65+
66+
assert response["success"] == 2
67+
68+
task1 = Repo.get_by(Task, name: "sum_of_two")
69+
assert task1
70+
assert task1.state == "active"
71+
assert task1.visibility == "hidden"
72+
assert task1.origin == "github"
73+
assert task1.level == "easy"
74+
assert task1.description_en == "Calculate sum of two numbers"
75+
assert length(task1.asserts) == 2
76+
77+
task2 = Repo.get_by(Task, name: "multiply_numbers")
78+
assert task2
79+
assert task2.state == "active"
80+
assert task2.visibility == "hidden"
81+
assert task2.origin == "github"
82+
assert task2.level == "elementary"
83+
end
84+
85+
test "updates existing tasks", %{conn: conn} do
86+
insert(:task,
87+
name: "existing_task",
88+
description_en: "Old description",
89+
level: "easy",
90+
origin: "github"
91+
)
92+
93+
tasks_data = [
94+
%{
95+
name: "existing_task",
96+
description_en: "Updated description",
97+
description_ru: "Обновленное описание",
98+
level: "medium",
99+
asserts: [
100+
%{arguments: [1, 2], expected: 3}
101+
],
102+
input_signature: [
103+
%{argument_name: "a", type: %{name: "integer"}},
104+
%{argument_name: "b", type: %{name: "integer"}}
105+
],
106+
output_signature: %{type: %{name: "integer"}},
107+
examples: "updated examples"
108+
}
109+
]
110+
111+
payload = create_gzipped_payload(tasks_data)
112+
113+
response =
114+
conn
115+
|> put_req_header("x-auth-key", "x-key")
116+
|> post(
117+
Routes.ext_api_task_path(conn, :create, %{
118+
payload: payload,
119+
origin: "github",
120+
visibility: "public"
121+
})
122+
)
123+
|> json_response(201)
124+
125+
assert response["success"] == 1
126+
127+
updated_task = Repo.get_by(Task, name: "existing_task")
128+
assert updated_task.description_en == "Updated description"
129+
assert updated_task.level == "medium"
130+
assert updated_task.visibility == "public"
131+
end
132+
133+
test "handles partial failures gracefully", %{conn: conn} do
134+
tasks_data = [
135+
%{
136+
name: "valid_task",
137+
description_en: "Valid task",
138+
level: "easy",
139+
asserts: [
140+
%{arguments: [1, 1], expected: 2}
141+
],
142+
input_signature: [
143+
%{argument_name: "a", type: %{name: "integer"}}
144+
],
145+
output_signature: %{type: %{name: "integer"}},
146+
examples: "test"
147+
},
148+
%{
149+
name: "invalid_task"
150+
# Missing required fields
151+
}
152+
]
153+
154+
payload = create_gzipped_payload(tasks_data)
155+
156+
response =
157+
conn
158+
|> put_req_header("x-auth-key", "x-key")
159+
|> post(
160+
Routes.ext_api_task_path(conn, :create, %{
161+
payload: payload,
162+
origin: "github",
163+
visibility: "public"
164+
})
165+
)
166+
|> json_response(400)
167+
168+
assert response["success"] == 1
169+
assert is_list(response["errors"])
170+
assert length(response["errors"]) == 1
171+
172+
# Valid task should be created
173+
assert Repo.get_by(Task, name: "valid_task")
174+
175+
# Invalid task should not be created
176+
refute Repo.get_by(Task, name: "invalid_task")
177+
end
178+
179+
test "returns error for invalid gzip payload", %{conn: conn} do
180+
invalid_payload = Base.encode64("not a gzipped data")
181+
182+
response =
183+
conn
184+
|> put_req_header("x-auth-key", "x-key")
185+
|> post(
186+
Routes.ext_api_task_path(conn, :create, %{
187+
payload: invalid_payload,
188+
origin: "github",
189+
visibility: "public"
190+
})
191+
)
192+
|> json_response(400)
193+
194+
assert response["errors"]["payload"] == "Invalid gzipped payload format"
195+
end
196+
197+
test "returns error for invalid base64 encoding", %{conn: conn} do
198+
response =
199+
conn
200+
|> put_req_header("x-auth-key", "x-key")
201+
|> post(
202+
Routes.ext_api_task_path(conn, :create, %{
203+
payload: "not-base64!!!",
204+
origin: "external_api",
205+
visibility: "public"
206+
})
207+
)
208+
|> json_response(400)
209+
210+
assert response["errors"]["payload"] == "Invalid gzipped payload format"
211+
end
212+
213+
test "returns error for invalid JSON in payload", %{conn: conn} do
214+
gzipped_data = :zlib.gzip("not valid json")
215+
payload = Base.encode64(gzipped_data)
216+
217+
response =
218+
conn
219+
|> put_req_header("x-auth-key", "x-key")
220+
|> post(
221+
Routes.ext_api_task_path(conn, :create, %{
222+
payload: payload,
223+
origin: "external_api",
224+
visibility: "public"
225+
})
226+
)
227+
|> json_response(400)
228+
229+
assert response["errors"]["payload"] == "Invalid gzipped payload format"
230+
end
231+
232+
test "creates multiple tasks in batch", %{conn: conn} do
233+
tasks_data =
234+
Enum.map(1..10, fn i ->
235+
%{
236+
name: "batch_task_#{i}",
237+
description_en: "Batch task #{i}",
238+
level: "easy",
239+
asserts: [
240+
%{arguments: [i], expected: i * 2}
241+
],
242+
input_signature: [
243+
%{argument_name: "x", type: %{name: "integer"}}
244+
],
245+
output_signature: %{type: %{name: "integer"}},
246+
examples: "test #{i}"
247+
}
248+
end)
249+
250+
payload = create_gzipped_payload(tasks_data)
251+
252+
response =
253+
conn
254+
|> put_req_header("x-auth-key", "x-key")
255+
|> post(
256+
Routes.ext_api_task_path(conn, :create, %{
257+
payload: payload,
258+
origin: "github",
259+
visibility: "public"
260+
})
261+
)
262+
|> json_response(201)
263+
264+
assert response["success"] == 10
265+
266+
Enum.each(1..10, fn i ->
267+
task = Repo.get_by(Task, name: "batch_task_#{i}")
268+
assert task
269+
assert task.origin == "github"
270+
end)
271+
end
272+
end
273+
274+
# Helper function to create gzipped and base64-encoded payload
275+
defp create_gzipped_payload(tasks_data) do
276+
tasks_data
277+
|> Jason.encode!()
278+
|> :zlib.gzip()
279+
|> Base.encode64()
280+
end
281+
end

0 commit comments

Comments
 (0)