-
Artem Baranovskyi authored9fdc9f0b
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
/**
* The plugin uses the ASYST grading tool <https://transfer.hft-stuttgart.de/gitlab/ulrike.pado/ASYST>
* modified to work as a web endpoint.
*
* This file contains the quiz API tests for the local_asystgrade plugin.
*
* @package local_asystgrade
* @copyright 2024 Artem Baranov
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace local_asystgrade;
use advanced_testcase;
use coding_exception;
use context_course;
use dml_exception;
use Exception;
use local_asystgrade\api\client;
use local_asystgrade\api\http_client;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/phpunit/classes/advanced_testcase.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/generator/lib.php');
/**
* Class quiz_api_test
* @package local_asystgrade
* @covers \local_asystgrade
*/
final class quiz_api_test extends advanced_testcase {
/**
* Sets up the test environment by resetting the database and truncating quiz-related tables.
*
* @throws dml_exception
*/
protected function setUp(): void {
global $DB;
$this->resetAfterTest();
parent::setUp();
// Clear quiz-related tables to ensure a clean test environment.
$DB->execute('TRUNCATE TABLE {quiz_attempts}');
$DB->execute('TRUNCATE TABLE {quiz_slots}');
}
/**
* Tests the quiz API by creating quiz attempts and validating responses.
* @throws Exception
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
* @covers \local_asystgrade
*/
public function test_quiz_api(): void {
global $DB;
$generator = $this->getDataGenerator();
$quizgen = $generator->get_plugin_generator('mod_quiz');
$questiongen = $generator->get_plugin_generator('core_question');
// Create a course.
$course = $generator->create_course([
'fullname' => 'Test Course',
'shortname' => 'testcourse',
'category' => 1,
]);
// Create and enroll a teacher.
$teacher = $generator->create_user();
$teacherroleid = $DB->get_record('role', ['shortname' => 'teacher'])->id;
$generator->enrol_user($teacher->id, $course->id, $teacherroleid);
$this->setUser($teacher);
// Create a quiz in the course.
$quiz = $quizgen->create_instance([
'course' => $course->id,
'name' => 'Test Quiz',
'intro' => 'This is a test quiz.',
'attempts' => 1,
'timeopen' => time(),
'timeclose' => time() + 3600,
]);
// Create a question category.
$context = context_course::instance($course->id);
$category = $this->create_question_category($context->id);
// Create sample questions and add them to the quiz.
$questions = include_once('fakedata/questions.php');
foreach ($questions as $questiondata) {
$question = $this->create_question($questiongen, $questiondata, $category, $teacher->id, $context);
$this->add_question_to_quiz($quiz, $question);
}
// Create and enroll students.
$students = [];
for ($i = 0; $i < 7; $i++) {
$students[] = $generator->create_user();
$generator->enrol_user($students[$i]->id, $course->id, 'student');
}
// Create quiz attempts for students.
$flatanswers = array_merge(...array_map(function($q) {
return isset($q['answers']) ? array_keys($q['answers']) : [];
}, $questions));
foreach ($students as $student) {
$this->create_quiz_attempt($quiz->id, $student->id, $flatanswers[array_rand($flatanswers)]);
}
$referenceanswers = [];
foreach ($questions as $questiondata) {
if (isset($questiondata['answers']) && is_array($questiondata['answers'])) {
foreach ($questiondata['answers'] as $answertext => $fraction) {
if ($fraction == 1) {
$referenceanswers[] = $answertext;
}
}
}
}
$requestdata = [
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
'studentAnswers' => $flatanswers,
'referenceAnswer' => array_fill(0, count($flatanswers), $referenceanswers[0] ?? ''),
];
$this->send_answers_to_api($requestdata);
}
/**
* Creates a question category.
*
* @param int $contextid
* @return false|mixed|\stdClass
* @throws coding_exception
* @throws dml_exception
*/
private function create_question_category(int $contextid): mixed {
global $DB;
$category = [
'name' => 'Test Category',
'contextid' => $contextid,
'parent' => 0,
'info' => '',
'infoformat' => FORMAT_MOODLE,
];
$categoryid = $DB->insert_record('question_categories', (object)$category);
if (!$categoryid) {
throw new coding_exception("Failed to create question category.");
}
return $DB->get_record('question_categories', ['id' => $categoryid]);
}
/**
* Creates a question and adds it to a category.
*
* @param $questiongen
* @param $questiondata
* @param $category
* @param $modifiedby
* @param $context
* @return mixed
*/
private function create_question($questiongen, $questiondata, $category, $modifiedby, $context): mixed {
return $questiongen->create_question($questiondata['qtype'], null, [
'category' => $category->id,
'questiontext' => ['text' => $questiondata['questiontext'], 'format' => FORMAT_HTML],
'name' => 'Test Question',
'contextid' => $context->id,
'modifiedby' => $modifiedby,
]);
}
/**
* Adds a question to a quiz.
*
* @throws dml_exception
* @param $quiz
* @param $question
* @return void
* @throws dml_exception
*/
private function add_question_to_quiz($quiz, $question): void {
global $DB;
$slotdata = [
'quizid' => $quiz->id,
'questionid' => $question->id,
'slot' => $DB->get_field_sql(
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
"SELECT COALESCE(MAX(slot), 0) + 1 FROM {quiz_slots} WHERE quizid = ?",
[$quiz->id]
),
'page' => 1,
'requireprevious' => 0,
'maxmark' => 1.0,
];
$DB->insert_record('quiz_slots', (object)$slotdata);
}
/**
* Creates a quiz attempt for a student.
* @throws dml_exception
*
* @param $quizid
* @param $userid
* @param $answer
* @return void
* @throws dml_exception
*/
private function create_quiz_attempt($quizid, $userid, $answer): void {
global $DB;
$uniqueid = $DB->get_field_sql('SELECT COALESCE(MAX(uniqueid), 0) + 1 FROM {quiz_attempts}');
$attempt = [
'quiz' => $quizid,
'userid' => $userid,
'attempt' => 1,
'uniqueid' => $uniqueid,
'state' => 'finished',
'timefinish' => time(),
'layout' => '',
];
$DB->insert_record('quiz_attempts', (object)$attempt);
// Add student answer (mocked data).
$DB->insert_record('question_attempt_step_data', [
'attemptstepid' => $uniqueid,
'name' => 'answer',
'value' => $answer,
]);
}
/**
* Sends answers to the API and verifies the response.
*
* @param $requestdata
* @return void
*/
private function send_answers_to_api($requestdata): void {
try {
$apiendpoint = utils::get_api_endpoint();
$httpclient = new http_client();
$apiclient = client::getInstance($apiendpoint, $httpclient);
$response = $apiclient->send_data($requestdata);
$grades = json_decode($response, true);
$this->assertNotEmpty($grades, 'API returned empty grades.');
} catch (Exception $e) {
debugging('Error: ' . $e->getMessage());
}
}
}