Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
39.48% covered (danger)
39.48%
107 / 271
28.57% covered (danger)
28.57%
6 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
TestDAO
39.48% covered (danger)
39.48%
107 / 271
28.57% covered (danger)
28.57%
6 / 21
413.56
0.00% covered (danger)
0.00%
0 / 1
 getTestByPerson
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 createTest
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 getTestById
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 addTestReview
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 addUnitReview
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 getTestState
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getTestSession
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 updateTestState
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
3.00
 getUnitState
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 updateUnitState
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 lockTest
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 changeTestLockStatus
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getOrCreateUnitId
38.46% covered (danger)
38.46%
10 / 26
0.00% covered (danger)
0.00%
0 / 1
7.73
 getDataParts
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 updateDataParts
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 deleteAttachmentDataPart
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 addUnitLog
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 addTestLog
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 setTestRunning
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getCommands
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
3
 setCommandExecuted
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3/** @noinspection PhpUnhandledExceptionInspection */
4declare(strict_types=1);
5
6class TestDAO extends DAO {
7  // TODO unit test
8  public function getTestByPerson(int $personId, string $bookletName): TestData|null {
9    $test = $this->_(
10      'select tests.locked, tests.name, tests.id, tests.laststate, tests.label, tests.running from tests
11            where tests.person_id=:personId and tests.name=:bookletname',
12      [
13        ':personId' => $personId,
14        ':bookletname' => $bookletName
15      ]
16    );
17
18    if (!$test) {
19      return null;
20    }
21    return new TestData(
22      $test['id'],
23      $test['name'],
24      $test['label'],
25      '',
26      (bool) $test['locked'],
27      (bool) $test['running'],
28      JSON::decode($test['laststate'])
29    );
30  }
31
32  // TODO unit test
33  public function createTest(int $personId, string $bookletId, string $bookletLabel): TestData {
34    $this->_(
35      'insert into tests (person_id, name, label) values (:person_id, :name, :label)',
36      [
37        ':person_id' => $personId,
38        ':name' => $bookletId,
39        ':label' => $bookletLabel
40      ]
41    );
42
43    return new TestData(
44      (int) $this->pdoDBhandle->lastInsertId(),
45      $bookletId,
46      $bookletLabel,
47      '',
48      false,
49      false,
50      (object) []
51    );
52  }
53
54  // TODO unit test
55  public function getTestById(int $testId): TestData|null {
56    $test = $this->_(
57      'select tests.locked, tests.name, tests.id, tests.laststate, tests.label, tests.running from tests where id = :id',
58      [
59        ':id' => $testId
60      ]
61    );
62
63    if (!$test) {
64      return null;
65    }
66
67    return new TestData(
68      $test['id'],
69      $test['name'],
70      $test['label'],
71      '',
72      (bool) $test['locked'],
73      (bool) $test['running'],
74      JSON::decode($test['laststate'])
75    );
76  }
77
78  // TODO unit test
79  public function addTestReview(
80    int $testId,
81    int $priority,
82    string $categories,
83    string $entry,
84    string $userAgent
85  ): void {
86    $this->_(
87      'insert into test_reviews (booklet_id, reviewtime, priority, categories, entry, user_agent) values(:b, :t, :p, :c, :e, :u)',
88      [
89        ':b' => $testId,
90        ':t' => TimeStamp::toSQLFormat(TimeStamp::now()),
91        ':p' => $priority,
92        ':c' => $categories,
93        ':e' => $entry,
94        ':u' => $userAgent
95      ]
96    );
97  }
98
99  // TODO unit test
100  public function addUnitReview(
101    int $testId,
102    string $unit,
103    int $priority,
104    string $categories,
105    string $entry,
106    string $userAgent,
107    string $originalUnitId,
108    ?int $page = null,
109    ?string $pageLabel = null,
110  ): void {
111    $unitDbId = $this->getOrCreateUnitId($testId, $unit, $originalUnitId);
112    $this->_(
113      'insert into unit_reviews (unit_id, reviewtime, priority, categories, entry, page, pagelabel, user_agent) values(:u, :t, :p, :c, :e, :pa, :pl, :ua)',
114      [
115        ':u' => $unitDbId,
116        ':t' => TimeStamp::toSQLFormat(TimeStamp::now()),
117        ':p' => $priority,
118        ':c' => $categories,
119        ':e' => $entry,
120        ':pa' => $page,
121        ':pl' => $pageLabel,
122        ':ua' => $userAgent,
123      ]
124    );
125  }
126
127  public function getTestState(int $testId): array {
128    $test = $this->_(
129      'select tests.laststate from tests where tests.id=:testId',
130      [
131        ':testId' => $testId
132      ]
133    );
134
135    return ($test) ? JSON::decode($test['laststate'], true) : [];
136  }
137
138  public function getTestSession(int $testId): array {
139    $testSession = $this->_(
140      'select
141        login_sessions.id as login_id,
142        logins.mode,
143        login_sessions.workspace_id,
144        logins.group_name as group_name,
145        login_sessions.token as login_token,
146        person_sessions.code,
147        person_sessions.token as person_token,
148        tests.person_id, 
149        tests.laststate as testState,
150        tests.id,
151        tests.locked,
152        tests.running,
153        tests.label
154      from 
155        tests 
156        left join person_sessions on person_sessions.id = tests.person_id
157        left join login_sessions on person_sessions.login_sessions_id = login_sessions.id
158        left join logins on logins.name = login_sessions.name
159      where 
160        tests.id=:testId',
161      [
162        ':testId' => $testId
163      ]
164    );
165
166    if ($testSession == null) {
167      throw new HttpError("Test not found", 404);
168    }
169
170    $testSession['laststate'] = $this->getTestFullState($testSession);
171
172    return $testSession;
173  }
174
175  // TODO use data-collection class for $statePatch (key-vale pairs)
176  public function updateTestState(int $testId, array $statePatch): array {
177    $testData = $this->_(
178      'select tests.laststate from tests where tests.id=:testId',
179      [
180        ':testId' => $testId
181      ]
182    );
183
184    if ($testData == null) {
185      throw new HttpError("Test not found", 404);
186    }
187
188    $oldState = $testData['laststate'] ? JSON::decode($testData['laststate'], true) : [];
189    $newState = array_merge($oldState, $statePatch);
190
191    $this->_(
192      'update tests set laststate = :laststate, timestamp_server = :timestamp where id = :id',
193      [
194        ':laststate' => json_encode($newState),
195        ':id' => $testId,
196        ':timestamp' => TimeStamp::toSQLFormat(TimeStamp::now())
197      ]
198    );
199
200    return $newState;
201  }
202
203  public function getUnitState(int $testId, string $unitName): array {
204    $unitData = $this->_(
205      'select units.laststate from units where units.name = :unitname and units.booklet_id = :testId',
206      [
207        ':unitname' => $unitName,
208        ':testId' => $testId
209      ]
210    );
211
212    return $unitData ? JSON::decode($unitData['laststate'], true) : [];
213  }
214
215  // TODO unit test
216  public function updateUnitState(int $testId, string $unitName, array $statePatch, string $originalUnitId = ''): array {
217    $unitDbId = $this->getOrCreateUnitId($testId, $unitName, $originalUnitId);
218
219    $unitData = $this->_(
220      'select units.laststate from units where units.id=:unitId',
221      [
222        ':unitId' => $unitDbId
223      ]
224    );
225
226    $oldState = $unitData['laststate'] ? JSON::decode($unitData['laststate'], true) : [];
227    $newState = array_merge($oldState, $statePatch);
228
229    // todo save states in separate key-value table instead of JSON blob
230    $this->_(
231      'update units set laststate = :laststate where id = :id',
232      [
233        ':laststate' => json_encode($newState),
234        ':id' => $unitDbId
235      ]
236    );
237
238    return $newState;
239  }
240
241  // TODO unit test
242  public function lockTest(int $testId): void {
243    $this->_(
244      'update tests set locked = :locked , timestamp_server = :timestamp where id = :id',
245      [
246        ':locked' => '1',
247        ':id' => $testId,
248        ':timestamp' => TimeStamp::toSQLFormat(TimeStamp::now())
249      ]
250    );
251  }
252
253  // TODO unit test
254  public function changeTestLockStatus(int $testId, bool $unlock = true): void {
255    $this->_(
256      'update tests set locked = :locked , timestamp_server = :timestamp where id = :id',
257      [
258        ':locked' => $unlock ? '0' : '1',
259        ':id' => $testId,
260        ':timestamp' => TimeStamp::toSQLFormat(TimeStamp::now())
261      ]
262    );
263  }
264
265
266  // TODO unit test
267  // todo reduce nr of queries by using replace...into syntax
268  private function getOrCreateUnitId(int $testId, string $unitName, string $originalUnitId = ''): string {
269    $unit = $this->_(
270      'select units.id from units where units.name = :unitname and units.booklet_id = :testId',
271      [
272        ':unitname' => $unitName,
273        ':testId' => $testId
274      ]
275    );
276
277    if ($unit && !empty($originalUnitId)) {
278      $this->_(
279        'update units set original_unit_id = :originalUnitId where id = :unitId',
280        [
281          ':unitId' => $unit['id'],
282          ':originalUnitId' => $originalUnitId
283        ]
284      );
285    }
286
287    if (!$unit) {
288      $this->_(
289        'insert into units (booklet_id, name, original_unit_id) values(:testId, :name, :originalUnitId)',
290        [
291          ':testId' => $testId,
292          ':name' => $unitName,
293          ':originalUnitId' => $originalUnitId
294        ]
295      );
296      return $this->pdoDBhandle->lastInsertId();
297    }
298
299    return (string) $unit['id'];
300  }
301
302  public function getDataParts(int $testId, string $unitName): array {
303    $result = $this->_(
304      'select
305          unit_data.part_id,
306          unit_data.content,
307          unit_data.response_type
308        from
309          unit_data
310          left join units on units.id = unit_data.unit_id
311        where
312          units.name = :unitname
313          and units.booklet_id = :testId
314        ',
315      [
316        ':unitname' => $unitName,
317        ':testId' => $testId
318      ],
319      true
320    );
321
322    $unitData = [];
323    foreach ($result as $row) {
324      $unitData[$row['part_id']] = $row['content'];
325    }
326
327    return [
328      "dataParts" => $unitData,
329      "dataType" => $row['response_type'] ?? ''
330    ];
331  }
332
333  public function updateDataParts(int $testId, string $unitName, array $dataParts, string $type, int $timestamp, string $originalUnitId = ''): void {
334    $unitDbId = $this->getOrCreateUnitId($testId, $unitName, $originalUnitId);
335    foreach ($dataParts as $partId => $content) {
336      $this->_(
337        'replace into unit_data(unit_id, part_id, content, ts, response_type)
338                          values (:unit_id, :part_id, :content, :ts, :response_type)',
339        [
340          ':part_id' => $partId,
341          ':content' => $content,
342          ':ts' => $timestamp,
343          ':response_type' => $type,
344          ':unit_id' => $unitDbId
345        ]
346      );
347    }
348  }
349
350  // TODO unit test
351  public function deleteAttachmentDataPart(string $partId): void {
352    // unitId is not necessary for identification, because partId contains unitName and TestId in case of attachments
353    $this->_(
354      'delete from unit_data where part_id = :partId',
355      [':partId' => $partId]
356    );
357  }
358
359  // TODO unit test
360  public function addUnitLog(
361    int $testId,
362    string $unitName,
363    string $logKey,
364    int $timestamp,
365    string $logContent = "",
366    string $originalUnitId = ''
367  ): void {
368    $unitId = $this->getOrCreateUnitId($testId, $unitName, $originalUnitId);
369
370    $this->_(
371      'insert into unit_logs (unit_id, logentry, timestamp) values (:unitId, :logentry, :ts)',
372      [
373        ':unitId' => $unitId,
374        ':logentry' => $logKey . ($logContent ? ' = ' . $logContent : ''),
375        ':ts' => $timestamp
376      ]
377    );
378  }
379
380  // TODO unit test
381  public function addTestLog(int $testId, string $logKey, int $timestamp, string $logContent = ""): void {
382    $this->_(
383      'insert into test_logs (booklet_id, logentry, timestamp) values (:bookletId, :logentry, :timestamp)',
384      [
385        ':bookletId' => $testId,
386        ':logentry' => $logKey . ($logContent ? ' : ' . $logContent : ''),
387        // TODO add value-column to log-tables instead of this shit
388        ':timestamp' => $timestamp
389      ]
390    );
391  }
392
393  // TODO unit test
394  public function setTestRunning(int $testId): void {
395    $this->_(
396      'update tests set running = :running , timestamp_server = :timestamp where id = :id',
397      [
398        ':running' => '1',
399        ':id' => $testId,
400        ':timestamp' => TimeStamp::toSQLFormat(TimeStamp::now())
401      ]
402    );
403  }
404
405  public function getCommands(int $testId, ?int $lastCommandId = null): array {
406    $sql = "select * from test_commands where test_id = :test_id and executed = 0 order by timestamp";
407    $replacements = [':test_id' => $testId];
408    if ($lastCommandId) {
409      $replacements[':last_id'] = $lastCommandId;
410      $sql = str_replace(
411        'where',
412        'where timestamp > (select timestamp from test_commands where id = :last_id) and ',
413        $sql
414      );
415    }
416
417    $commands = [];
418    foreach ($this->_($sql, $replacements, true) as $line) {
419      $commands[] = new Command(
420        (int) $line['id'],
421        $line['keyword'],
422        TimeStamp::fromSQLFormat($line['timestamp']),
423        ...JSON::decode($line['parameter'], true)
424      );
425    }
426    return $commands;
427  }
428
429  public function setCommandExecuted(int $testId, int $commandId): bool {
430    $command = $this->_(
431      'select executed from test_commands where test_id = :testId and id = :commandId',
432      [':testId' => $testId, ':commandId' => $commandId]
433    );
434
435    if (!$command) {
436      throw new HttpError("Command `$commandId` not found on test `$testId`", 404);
437    }
438
439    if ($command['executed']) {
440      return false;
441    }
442
443    $this->_(
444      'update test_commands set executed = 1 where test_id = :testId and id = :commandId',
445      [':testId' => $testId, ':commandId' => $commandId]
446    );
447
448    return true;
449  }
450}