Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
185 / 185
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
ExtensionTestingReportService
100.00% covered (success)
100.00%
185 / 185
100.00% covered (success)
100.00%
7 / 7
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTemplate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 listReports
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createReport
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 updateReport
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 deleteReport
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 buildTemplateData
100.00% covered (success)
100.00%
160 / 160
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Http\Services;
4
5use App\Http\Models\ExtensionTestingReport;
6use App\Http\Repositories\ExtensionTestingReportRepository;
7use Illuminate\Auth\Access\AuthorizationException;
8use Illuminate\Support\Collection;
9use Illuminate\Validation\ValidationException;
10
11/**
12 * Service for Extension Testing Report business logic.
13 *
14 * Orchestrates testing report operations including template retrieval,
15 * CRUD operations, and ownership enforcement.
16 */
17class ExtensionTestingReportService
18{
19    public function __construct(
20        private ExtensionTestingReportRepository $repository
21    ) {}
22
23    /**
24     * Get the predefined domain/test-case template.
25     *
26     * Returns the static template used to seed new reports with all domains,
27     * test areas, and test cases pre-populated (results null, notes empty).
28     *
29     * @return array<int, array{id: string, label: string, hostname: string, icon: string, isCustom: bool, testAreas: array}>
30     */
31    public function getTemplate(): array
32    {
33        return $this->buildTemplateData();
34    }
35
36    /**
37     * List all testing reports with optional filters.
38     *
39     * @param  array{artifact_id?: string, environment?: string, status?: string}  $filters
40     * @return Collection<int, ExtensionTestingReport>
41     */
42    public function listReports(array $filters = []): Collection
43    {
44        return $this->repository->listAll($filters);
45    }
46
47    /**
48     * Create a new testing report.
49     *
50     * Enforces one report per user per artifact. When no domains are provided,
51     * seeds the report with the full template (all results null).
52     *
53     * @param  array{name: string, version_tested: string, environment: string, artifact_id: string, tester_name: string, status?: string, domains?: array}  $data
54     * @param  string  $userId  The authenticated user's ID
55     *
56     * @throws ValidationException When a report already exists for this user/artifact combination
57     */
58    public function createReport(array $data, string $userId): ExtensionTestingReport
59    {
60        $existing = $this->repository->findByArtifactAndUser($data['artifact_id'], $userId);
61
62        if ($existing) {
63            throw ValidationException::withMessages([
64                'artifact_id' => ['A report for this artifact already exists for the current user.'],
65            ]);
66        }
67
68        $data['created_by'] = $userId;
69        $data['status'] = $data['status'] ?? ExtensionTestingReport::STATUS_IN_PROGRESS;
70
71        if (empty($data['domains'])) {
72            $data['domains'] = $this->buildTemplateData();
73        }
74
75        return $this->repository->create($data);
76    }
77
78    /**
79     * Update an existing testing report.
80     *
81     * Only the report owner can update it.
82     *
83     * @param  string  $id  The report ID
84     * @param  array{name?: string, version_tested?: string, status?: string, domains?: array, tester_name?: string}  $data
85     * @param  string  $userId  The authenticated user's ID
86     *
87     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException When report is not found
88     * @throws AuthorizationException When user is not the report owner
89     */
90    public function updateReport(string $id, array $data, string $userId): ExtensionTestingReport
91    {
92        $report = $this->repository->findById($id);
93
94        if (! $report) {
95            abort(404, 'Testing report not found.');
96        }
97
98        if ($report->created_by !== $userId) {
99            throw new AuthorizationException('You are not authorized to update this report.');
100        }
101
102        return $this->repository->update($report, $data);
103    }
104
105    /**
106     * Delete a testing report.
107     *
108     * Only the report owner can delete it.
109     *
110     * @param  string  $id  The report ID
111     * @param  string  $userId  The authenticated user's ID
112     *
113     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException When report is not found
114     * @throws AuthorizationException When user is not the report owner
115     */
116    public function deleteReport(string $id, string $userId): bool
117    {
118        $report = $this->repository->findById($id);
119
120        if (! $report) {
121            abort(404, 'Testing report not found.');
122        }
123
124        if ($report->created_by !== $userId) {
125            throw new AuthorizationException('You are not authorized to delete this report.');
126        }
127
128        return $this->repository->delete($report);
129    }
130
131    /**
132     * Build the predefined domain/test-case template data.
133     *
134     * This template matches the frontend TestingReport data structure.
135     * All test case results are null and notes are empty strings.
136     *
137     * @return array<int, array{id: string, label: string, hostname: string, icon: string, isCustom: bool, testAreas: array}>
138     */
139    private function buildTemplateData(): array
140    {
141        return [
142            [
143                'id' => 'gmail',
144                'label' => 'Gmail',
145                'hostname' => 'mail.google.com',
146                'icon' => 'mail',
147                'isCustom' => false,
148                'testAreas' => [
149                    [
150                        'id' => 'gmail_compose',
151                        'name' => 'Compose Email',
152                        'isCustom' => false,
153                        'testCases' => [
154                            ['id' => 'gmail_compose_to', 'title' => 'To Field', 'instructions' => 'Can type recipient in the To field', 'result' => null, 'notes' => '', 'isCustom' => false],
155                            ['id' => 'gmail_compose_cc', 'title' => 'CC Field', 'instructions' => 'Can add CC recipients', 'result' => null, 'notes' => '', 'isCustom' => false],
156                            ['id' => 'gmail_compose_body', 'title' => 'Body', 'instructions' => 'FlyMSG deploys correctly in the email body', 'result' => null, 'notes' => '', 'isCustom' => false],
157                        ],
158                    ],
159                    [
160                        'id' => 'gmail_grammar',
161                        'name' => 'Grammar',
162                        'isCustom' => false,
163                        'testCases' => [
164                            ['id' => 'gmail_grammar_enable', 'title' => 'Enable', 'instructions' => 'Grammar checker appears when typing', 'result' => null, 'notes' => '', 'isCustom' => false],
165                            ['id' => 'gmail_grammar_disable', 'title' => 'Disable', 'instructions' => 'Can toggle grammar off', 'result' => null, 'notes' => '', 'isCustom' => false],
166                            ['id' => 'gmail_grammar_replace', 'title' => 'Replace', 'instructions' => 'Clicking Replace applies the suggestion', 'result' => null, 'notes' => '', 'isCustom' => false],
167                            ['id' => 'gmail_grammar_ignore', 'title' => 'Ignore', 'instructions' => 'Can dismiss a grammar suggestion', 'result' => null, 'notes' => '', 'isCustom' => false],
168                        ],
169                    ],
170                    [
171                        'id' => 'gmail_paragraph',
172                        'name' => 'Paragraph',
173                        'isCustom' => false,
174                        'testCases' => [
175                            ['id' => 'gmail_para_select_all', 'title' => 'Select All', 'instructions' => 'Select all paragraphs option works', 'result' => null, 'notes' => '', 'isCustom' => false],
176                            ['id' => 'gmail_para_shortcut', 'title' => 'Select by shortcut', 'instructions' => 'Shortcut selects a paragraph', 'result' => null, 'notes' => '', 'isCustom' => false],
177                            ['id' => 'gmail_para_single', 'title' => 'Select single paragraph/line', 'instructions' => 'Can select an individual paragraph', 'result' => null, 'notes' => '', 'isCustom' => false],
178                            ['id' => 'gmail_para_replace', 'title' => 'Replace', 'instructions' => 'Replace paragraph with a shortcut content', 'result' => null, 'notes' => '', 'isCustom' => false],
179                            ['id' => 'gmail_para_continue', 'title' => 'Continue Writing', 'instructions' => 'AI continues writing from cursor position', 'result' => null, 'notes' => '', 'isCustom' => false],
180                            ['id' => 'gmail_para_dismiss', 'title' => 'Dismiss', 'instructions' => 'Can dismiss the paragraph panel', 'result' => null, 'notes' => '', 'isCustom' => false],
181                        ],
182                    ],
183                    [
184                        'id' => 'gmail_shortcut',
185                        'name' => 'Shortcut',
186                        'isCustom' => false,
187                        'testCases' => [
188                            ['id' => 'gmail_shortcut_manual', 'title' => 'Manual Deploy', 'instructions' => 'Manually typing shortcut deploys FlyMSG', 'result' => null, 'notes' => '', 'isCustom' => false],
189                            ['id' => 'gmail_shortcut_fms', 'title' => 'FMS Button', 'instructions' => 'FMS button appears and deploys FlyMSG', 'result' => null, 'notes' => '', 'isCustom' => false],
190                            ['id' => 'gmail_shortcut_plugin', 'title' => 'Plugin Deploy', 'instructions' => 'Shortcut via plugin extension deploys correctly', 'result' => null, 'notes' => '', 'isCustom' => false],
191                        ],
192                    ],
193                    [
194                        'id' => 'gmail_quick_insert',
195                        'name' => 'Quick Insert',
196                        'isCustom' => false,
197                        'testCases' => [
198                            ['id' => 'gmail_qi_works', 'title' => 'Quick Insert', 'instructions' => 'Can quick-insert content into the field', 'result' => null, 'notes' => '', 'isCustom' => false],
199                            ['id' => 'gmail_qi_simple', 'title' => 'Simple Text Shortcut', 'instructions' => 'Plain text shortcuts insert correctly', 'result' => null, 'notes' => '', 'isCustom' => false],
200                            ['id' => 'gmail_qi_rich', 'title' => 'Rich Text Shortcut', 'instructions' => 'Rich text shortcuts insert with formatting preserved', 'result' => null, 'notes' => '', 'isCustom' => false],
201                            ['id' => 'gmail_qi_manual', 'title' => 'Manual Shortcut', 'instructions' => 'Manually triggered shortcuts work', 'result' => null, 'notes' => '', 'isCustom' => false],
202                        ],
203                    ],
204                ],
205            ],
206            [
207                'id' => 'linkedin',
208                'label' => 'LinkedIn',
209                'hostname' => 'linkedin.com',
210                'icon' => 'people',
211                'isCustom' => false,
212                'testAreas' => [
213                    [
214                        'id' => 'linkedin_posts',
215                        'name' => 'Posts',
216                        'isCustom' => false,
217                        'testCases' => [
218                            ['id' => 'linkedin_posts_create', 'title' => 'Create post', 'instructions' => 'FlyMSG is available in the post composer', 'result' => null, 'notes' => '', 'isCustom' => false],
219                            ['id' => 'linkedin_posts_shortcut', 'title' => 'Shortcut in post', 'instructions' => 'Shortcut deploys FlyMSG inside a post', 'result' => null, 'notes' => '', 'isCustom' => false],
220                            ['id' => 'linkedin_posts_grammar', 'title' => 'Grammar in post', 'instructions' => 'Grammar checker works in a post', 'result' => null, 'notes' => '', 'isCustom' => false],
221                            ['id' => 'linkedin_posts_paragraph', 'title' => 'Paragraph in post', 'instructions' => 'Paragraph tool works in a post', 'result' => null, 'notes' => '', 'isCustom' => false],
222                        ],
223                    ],
224                    [
225                        'id' => 'linkedin_comments',
226                        'name' => 'Comments',
227                        'isCustom' => false,
228                        'testCases' => [
229                            ['id' => 'linkedin_comments_create', 'title' => 'Create comment', 'instructions' => 'FlyMSG is available in the comment box', 'result' => null, 'notes' => '', 'isCustom' => false],
230                            ['id' => 'linkedin_comments_shortcut', 'title' => 'Shortcut in comment', 'instructions' => 'Shortcut deploys FlyMSG inside a comment', 'result' => null, 'notes' => '', 'isCustom' => false],
231                            ['id' => 'linkedin_comments_grammar', 'title' => 'Grammar in comment', 'instructions' => 'Grammar checker works in a comment', 'result' => null, 'notes' => '', 'isCustom' => false],
232                        ],
233                    ],
234                ],
235            ],
236            [
237                'id' => 'outlook',
238                'label' => 'Outlook',
239                'hostname' => 'outlook.com',
240                'icon' => 'inbox',
241                'isCustom' => false,
242                'testAreas' => [
243                    [
244                        'id' => 'outlook_compose',
245                        'name' => 'Compose Email',
246                        'isCustom' => false,
247                        'testCases' => [
248                            ['id' => 'outlook_compose_to', 'title' => 'To Field', 'instructions' => 'Can type recipient in the To field', 'result' => null, 'notes' => '', 'isCustom' => false],
249                            ['id' => 'outlook_compose_cc', 'title' => 'CC Field', 'instructions' => 'Can add CC recipients', 'result' => null, 'notes' => '', 'isCustom' => false],
250                            ['id' => 'outlook_compose_body', 'title' => 'Body', 'instructions' => 'FlyMSG deploys correctly in the email body', 'result' => null, 'notes' => '', 'isCustom' => false],
251                        ],
252                    ],
253                    [
254                        'id' => 'outlook_grammar',
255                        'name' => 'Grammar',
256                        'isCustom' => false,
257                        'testCases' => [
258                            ['id' => 'outlook_grammar_enable', 'title' => 'Enable', 'instructions' => 'Grammar checker appears when typing', 'result' => null, 'notes' => '', 'isCustom' => false],
259                            ['id' => 'outlook_grammar_disable', 'title' => 'Disable', 'instructions' => 'Can toggle grammar off', 'result' => null, 'notes' => '', 'isCustom' => false],
260                            ['id' => 'outlook_grammar_replace', 'title' => 'Replace', 'instructions' => 'Clicking Replace applies the suggestion', 'result' => null, 'notes' => '', 'isCustom' => false],
261                            ['id' => 'outlook_grammar_ignore', 'title' => 'Ignore', 'instructions' => 'Can dismiss a grammar suggestion', 'result' => null, 'notes' => '', 'isCustom' => false],
262                        ],
263                    ],
264                    [
265                        'id' => 'outlook_paragraph',
266                        'name' => 'Paragraph',
267                        'isCustom' => false,
268                        'testCases' => [
269                            ['id' => 'outlook_para_select_all', 'title' => 'Select All', 'instructions' => 'Select all paragraphs option works', 'result' => null, 'notes' => '', 'isCustom' => false],
270                            ['id' => 'outlook_para_shortcut', 'title' => 'Select by shortcut', 'instructions' => 'Shortcut selects a paragraph', 'result' => null, 'notes' => '', 'isCustom' => false],
271                            ['id' => 'outlook_para_single', 'title' => 'Select single paragraph/line', 'instructions' => 'Can select an individual paragraph', 'result' => null, 'notes' => '', 'isCustom' => false],
272                            ['id' => 'outlook_para_replace', 'title' => 'Replace', 'instructions' => 'Replace paragraph with a shortcut content', 'result' => null, 'notes' => '', 'isCustom' => false],
273                            ['id' => 'outlook_para_continue', 'title' => 'Continue Writing', 'instructions' => 'AI continues writing from cursor position', 'result' => null, 'notes' => '', 'isCustom' => false],
274                            ['id' => 'outlook_para_dismiss', 'title' => 'Dismiss', 'instructions' => 'Can dismiss the paragraph panel', 'result' => null, 'notes' => '', 'isCustom' => false],
275                        ],
276                    ],
277                    [
278                        'id' => 'outlook_shortcut',
279                        'name' => 'Shortcut',
280                        'isCustom' => false,
281                        'testCases' => [
282                            ['id' => 'outlook_shortcut_manual', 'title' => 'Manual Deploy', 'instructions' => 'Manually typing shortcut deploys FlyMSG', 'result' => null, 'notes' => '', 'isCustom' => false],
283                            ['id' => 'outlook_shortcut_fms', 'title' => 'FMS Button', 'instructions' => 'FMS button appears and deploys FlyMSG', 'result' => null, 'notes' => '', 'isCustom' => false],
284                            ['id' => 'outlook_shortcut_plugin', 'title' => 'Plugin Deploy', 'instructions' => 'Shortcut via plugin extension deploys correctly', 'result' => null, 'notes' => '', 'isCustom' => false],
285                        ],
286                    ],
287                    [
288                        'id' => 'outlook_quick_insert',
289                        'name' => 'Quick Insert',
290                        'isCustom' => false,
291                        'testCases' => [
292                            ['id' => 'outlook_qi_works', 'title' => 'Quick Insert', 'instructions' => 'Can quick-insert content into the field', 'result' => null, 'notes' => '', 'isCustom' => false],
293                            ['id' => 'outlook_qi_simple', 'title' => 'Simple Text Shortcut', 'instructions' => 'Plain text shortcuts insert correctly', 'result' => null, 'notes' => '', 'isCustom' => false],
294                            ['id' => 'outlook_qi_rich', 'title' => 'Rich Text Shortcut', 'instructions' => 'Rich text shortcuts insert with formatting preserved', 'result' => null, 'notes' => '', 'isCustom' => false],
295                            ['id' => 'outlook_qi_manual', 'title' => 'Manual Shortcut', 'instructions' => 'Manually triggered shortcuts work', 'result' => null, 'notes' => '', 'isCustom' => false],
296                        ],
297                    ],
298                ],
299            ],
300        ];
301    }
302}