diff --git a/.github/workflows/specs-site.yml b/.github/workflows/specs-site.yml new file mode 100644 index 0000000000..95d469fd53 --- /dev/null +++ b/.github/workflows/specs-site.yml @@ -0,0 +1,52 @@ +name: Deploy Specs Site + +on: + push: + branches: [develop] + paths: + - 'docs/specs/**' + - 'specs-site/**' + + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + working-directory: specs-site + run: npm ci + + - name: Build site + working-directory: specs-site + run: npm run build + + - uses: actions/upload-pages-artifact@v3 + with: + path: specs-site/.vitepress/dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/CLAUDE.md b/CLAUDE.md index 6d24af4689..ac7283fe6f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -146,5 +146,17 @@ npm test - Event-driven architecture with model observers - Only translate fr and fr-BE +## Living Specifications + +When modifying PHP controller or service methods: +- Maintain `#[UserStory]` and `#[Feature]` attributes (in `app/Attributes/`) +- Add `#[UserStory]` to new public methods that represent user-facing functionality +- Add `#[NoStory]` to methods that intentionally have no user story +- Update the story text if you change what a method does +- When adding or modifying tests, include `@story:ClassName::method` references +- Run `php artisan specs:extract` after annotation changes and commit the updated manifest +- Update the narrative in `docs/specs/narratives/` if feature coverage has changed +- Preserve human-written prose in narratives -- update structure and counts, not wording + ## Development Warnings - Don't try to test changes when you're running on Windows. \ No newline at end of file diff --git a/app/Attributes/Feature.php b/app/Attributes/Feature.php new file mode 100644 index 0000000000..cc782aaa55 --- /dev/null +++ b/app/Attributes/Feature.php @@ -0,0 +1,14 @@ +info('Scanning for specifications...'); + + $features = []; + $this->scanPhpFiles($features); + $this->scanTestFiles($features); + + $personas = $this->buildPersonaIndex($features); + $coverage = $this->buildCoverageStats($features); + + $manifest = [ + 'generatedAt' => gmdate('Y-m-d\TH:i:s\Z'), + 'features' => $features, + 'personas' => $personas, + 'coverage' => $coverage, + ]; + + $outputPath = base_path('docs/specs/manifest.json'); + + if ($this->option('check')) { + return $this->checkManifest($manifest, $outputPath); + } + + if (! is_dir(dirname($outputPath))) { + mkdir(dirname($outputPath), 0755, true); + } + + // Write without generatedAt for deterministic output, then re-add for the file + $stableManifest = $manifest; + unset($stableManifest['generatedAt']); + $stableManifest = ['generatedAt' => $manifest['generatedAt']] + $stableManifest; + + file_put_contents($outputPath, json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"); + + $storyCount = array_sum(array_map(fn ($f) => $f['storyCount'], $features)); + $featureCount = count($features); + $personaCount = count($personas); + + $this->info("Extracted {$storyCount} stories across {$featureCount} features and {$personaCount} personas."); + $this->info("Manifest written to docs/specs/manifest.json"); + + $this->reportWarnings($features); + + return Command::SUCCESS; + } + + private function scanPhpFiles(array &$features): void + { + $parser = (new ParserFactory)->createForNewestSupportedVersion(); + $appPath = base_path('app'); + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($appPath, RecursiveDirectoryIterator::SKIP_DOTS) + ); + + foreach ($iterator as $file) { + if ($file->getExtension() !== 'php') { + continue; + } + + $code = file_get_contents($file->getPathname()); + $relativePath = str_replace(base_path() . '/', '', $file->getPathname()); + + try { + $ast = $parser->parse($code); + } catch (\Exception $e) { + $this->warn("Failed to parse {$relativePath}: {$e->getMessage()}"); + continue; + } + + if ($ast === null) { + continue; + } + + $this->extractFromAst($ast, $relativePath, $features); + } + + // Sort features alphabetically + ksort($features); + foreach ($features as &$feature) { + usort($feature['stories'], fn ($a, $b) => strcmp($a['persona'], $b['persona']) ?: strcmp($a['method'], $b['method'])); + sort($feature['personas']); + } + } + + private function extractFromAst(array $ast, string $filePath, array &$features): void + { + $traverser = new NodeTraverser(); + $visitor = new class extends NodeVisitorAbstract { + public ?string $namespace = null; + public ?string $className = null; + public ?array $classFeature = null; + public array $methods = []; + + public function enterNode(Node $node) + { + if ($node instanceof Node\Stmt\Namespace_) { + $this->namespace = $node->name ? $node->name->toString() : null; + } + + if ($node instanceof Node\Stmt\Class_) { + $this->className = $node->name ? $node->name->toString() : null; + $this->classFeature = $this->findFeatureAttribute($node); + } + + if ($node instanceof Node\Stmt\ClassMethod) { + $stories = $this->findUserStoryAttributes($node); + $noStory = $this->findNoStoryAttribute($node); + $isPublic = $node->isPublic(); + + $this->methods[] = [ + 'name' => $node->name->toString(), + 'stories' => $stories, + 'noStory' => $noStory, + 'isPublic' => $isPublic, + ]; + } + + return null; + } + + private function findFeatureAttribute(Node\Stmt\Class_ $node): ?array + { + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $name = $attr->name->toString(); + if ($name === 'Feature' || str_ends_with($name, '\\Feature')) { + $args = $this->parseAttributeArgs($attr); + return [ + 'name' => $args[0] ?? $args['name'] ?? '', + 'description' => $args['description'] ?? $args[1] ?? '', + ]; + } + } + } + return null; + } + + private function findUserStoryAttributes(Node\Stmt\ClassMethod $node): array + { + $stories = []; + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $name = $attr->name->toString(); + if ($name === 'UserStory' || str_ends_with($name, '\\UserStory')) { + $args = $this->parseAttributeArgs($attr); + $stories[] = [ + 'story' => $args[0] ?? $args['story'] ?? '', + 'persona' => $args['persona'] ?? $args[1] ?? '', + 'feature' => $args['feature'] ?? '', + 'theme' => $args['theme'] ?? '', + ]; + } + } + } + return $stories; + } + + private function findNoStoryAttribute(Node\Stmt\ClassMethod $node): ?string + { + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $name = $attr->name->toString(); + if ($name === 'NoStory' || str_ends_with($name, '\\NoStory')) { + $args = $this->parseAttributeArgs($attr); + return $args['reason'] ?? $args[0] ?? ''; + } + } + } + return null; + } + + private function parseAttributeArgs(Node\Attribute $attr): array + { + $args = []; + $positional = 0; + foreach ($attr->args as $arg) { + $value = $this->resolveValue($arg->value); + if ($arg->name) { + $args[$arg->name->toString()] = $value; + } else { + $args[$positional++] = $value; + } + } + return $args; + } + + private function resolveValue(Node\Expr $expr): mixed + { + if ($expr instanceof Node\Scalar\String_) { + return $expr->value; + } + if ($expr instanceof Node\Scalar\LNumber) { + return $expr->value; + } + if ($expr instanceof Node\Expr\ConstFetch) { + $name = $expr->name->toString(); + return match (strtolower($name)) { + 'true' => true, + 'false' => false, + 'null' => null, + default => $name, + }; + } + return '(complex expression)'; + } + }; + + $traverser->addVisitor($visitor); + $traverser->traverse($ast); + + if ($visitor->className === null) { + return; + } + + $classFeature = $visitor->classFeature; + $shortClass = $visitor->className; + + foreach ($visitor->methods as $method) { + foreach ($method['stories'] as $story) { + $featureName = $story['feature'] ?: ($classFeature ? $classFeature['name'] : 'Uncategorised'); + $featureDesc = $classFeature ? $classFeature['description'] : ''; + + if (! isset($features[$featureName])) { + $features[$featureName] = [ + 'description' => $featureDesc, + 'sources' => [], + 'stories' => [], + 'storyCount' => 0, + 'personas' => [], + ]; + } + + if ($featureDesc && ! $features[$featureName]['description']) { + $features[$featureName]['description'] = $featureDesc; + } + + if (! in_array($filePath, $features[$featureName]['sources'])) { + $features[$featureName]['sources'][] = $filePath; + } + + $features[$featureName]['stories'][] = [ + 'story' => $story['story'], + 'persona' => $story['persona'], + 'theme' => $story['theme'] ?: 'General', + 'method' => "{$shortClass}::{$method['name']}", + 'file' => $filePath, + 'tests' => [], + ]; + + if (! in_array($story['persona'], $features[$featureName]['personas'])) { + $features[$featureName]['personas'][] = $story['persona']; + } + + $features[$featureName]['storyCount'] = count($features[$featureName]['stories']); + } + } + } + + private function scanTestFiles(array &$features): void + { + $storyIndex = []; + foreach ($features as $featureName => &$feature) { + foreach ($feature['stories'] as $idx => &$story) { + $storyIndex[$story['method']][] = [ + 'feature' => $featureName, + 'index' => $idx, + ]; + } + } + + $testDirs = [ + base_path('tests'), + ]; + + foreach ($testDirs as $dir) { + if (! is_dir($dir)) { + continue; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS) + ); + + foreach ($iterator as $file) { + $ext = $file->getExtension(); + if (! in_array($ext, ['php', 'js', 'ts'])) { + continue; + } + + $content = file_get_contents($file->getPathname()); + $relativePath = str_replace(base_path() . '/', '', $file->getPathname()); + + preg_match_all('/@story:(\w+::\w+)/', $content, $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + $methodRef = $match[1]; + if (isset($storyIndex[$methodRef])) { + $testName = $this->extractTestName($content, $match[0], $ext); + + foreach ($storyIndex[$methodRef] as $ref) { + $features[$ref['feature']]['stories'][$ref['index']]['tests'][] = [ + 'file' => $relativePath, + 'test' => $testName, + ]; + } + } + } + } + } + } + + private function extractTestName(string $content, string $storyRef, string $ext): string + { + $lines = explode("\n", $content); + foreach ($lines as $line) { + if (str_contains($line, $storyRef)) { + if ($ext === 'php') { + if (preg_match('/function\s+(\w+)/', $line, $m)) { + return $m[1]; + } + // Check previous lines for function declaration + $lineIdx = array_search($line, $lines); + for ($i = $lineIdx; $i >= max(0, $lineIdx - 5); $i--) { + if (preg_match('/function\s+(\w+)/', $lines[$i], $m)) { + return $m[1]; + } + } + } else { + if (preg_match("/(?:test|it)\s*\(\s*['\"](.+?)['\"]/", $line, $m)) { + return $m[1]; + } + } + } + } + return '(unknown test)'; + } + + private function buildPersonaIndex(array $features): array + { + $personas = []; + foreach ($features as $featureName => $feature) { + foreach ($feature['stories'] as $story) { + $persona = $story['persona']; + if (! isset($personas[$persona])) { + $personas[$persona] = [ + 'features' => [], + 'storyCount' => 0, + ]; + } + if (! in_array($featureName, $personas[$persona]['features'])) { + $personas[$persona]['features'][] = $featureName; + } + $personas[$persona]['storyCount']++; + } + } + + ksort($personas); + foreach ($personas as &$persona) { + sort($persona['features']); + } + + return $personas; + } + + private function buildCoverageStats(array $features): array + { + $annotated = 0; + $withTests = 0; + + foreach ($features as $feature) { + foreach ($feature['stories'] as $story) { + $annotated++; + if (! empty($story['tests'])) { + $withTests++; + } + } + } + + return [ + 'annotatedStories' => $annotated, + 'storiesWithTests' => $withTests, + ]; + } + + private function reportWarnings(array $features): void + { + $uncovered = []; + foreach ($features as $featureName => $feature) { + foreach ($feature['stories'] as $story) { + if (empty($story['tests'])) { + $uncovered[] = "{$story['method']} ({$featureName})"; + } + } + } + + if ($uncovered) { + $this->warn('Stories without test coverage:'); + foreach ($uncovered as $method) { + $this->line(" - {$method}"); + } + } + } + + private function checkManifest(array $manifest, string $outputPath): int + { + if (! file_exists($outputPath)) { + $this->error('No manifest found at docs/specs/manifest.json. Run specs:extract to generate it.'); + return Command::FAILURE; + } + + $existing = json_decode(file_get_contents($outputPath), true); + + // Compare without generatedAt timestamp + $compareNew = $manifest; + $compareExisting = $existing; + unset($compareNew['generatedAt'], $compareExisting['generatedAt']); + + if ($compareNew === $compareExisting) { + $this->info('Manifest is up to date.'); + return Command::SUCCESS; + } + + $this->error('Manifest is out of date. Run php artisan specs:extract to update it.'); + return Command::FAILURE; + } +} diff --git a/app/Http/Controllers/API/AlertController.php b/app/Http/Controllers/API/AlertController.php index 26bda7d946..f925607ef8 100644 --- a/app/Http/Controllers/API/AlertController.php +++ b/app/Http/Controllers/API/AlertController.php @@ -12,7 +12,10 @@ use Illuminate\Http\Request; use Notification; use Illuminate\Validation\ValidationException; +use App\Attributes\Feature; +use App\Attributes\UserStory; +#[Feature('Administration', description: 'Platform administration and configuration')] class AlertController extends Controller { /** @@ -38,6 +41,7 @@ class AlertController extends Controller * ), * ) */ + #[UserStory('As a Guest, I can view active platform alerts', persona: 'Guest', theme: 'Platform alerts')] public function listAlertsv2(Request $request) { // Alerts don't change often, so we can cache them. if (\Cache::has('alerts')) { @@ -115,6 +119,7 @@ public function listAlertsv2(Request $request) { * ) * ) */ + #[UserStory('As an Admin, I can create a platform-wide alert', persona: 'Admin', theme: 'Platform alerts')] public function addAlertv2(Request $request) { $user = $this->getUser(); @@ -201,6 +206,7 @@ public function addAlertv2(Request $request) * ) * ) */ + #[UserStory('As an Admin, I can update a platform alert', persona: 'Admin', theme: 'Platform alerts')] public function updateAlertv2(Request $request, $id) { $user = $this->getUser(); diff --git a/app/Http/Controllers/API/DeviceController.php b/app/Http/Controllers/API/DeviceController.php index 674899da57..ccb9e6813b 100644 --- a/app/Http/Controllers/API/DeviceController.php +++ b/app/Http/Controllers/API/DeviceController.php @@ -19,7 +19,10 @@ use Carbon\Carbon; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\DB; +use App\Attributes\Feature; +use App\Attributes\UserStory; +#[Feature('Devices', description: 'Repair device tracking and impact measurement')] class DeviceController extends Controller { /** * @OA\Get( @@ -55,6 +58,8 @@ class DeviceController extends Controller { * ) */ + #[UserStory('As a Guest, I can view device details via the API', persona: 'Guest', theme: 'Get device details')] + #[UserStory('As a ThirdParty, I can retrieve device repair data via the API', persona: 'ThirdParty', theme: 'Get device details')] public function getDevicev2(Request $request, $iddevices) { $device = Device::findOrFail($iddevices); @@ -155,6 +160,7 @@ public function getDevicev2(Request $request, $iddevices) * ) * ) */ + #[UserStory('As a Restarter, I can log a device repair at an event I attended', persona: 'Restarter', theme: 'Log & edit repairs')] public function createDevicev2(Request $request) { $user = $this->getUser(); @@ -344,6 +350,7 @@ public function createDevicev2(Request $request) * ) * ) */ + #[UserStory('As a Restarter, I can update a device repair record at an event I attended', persona: 'Restarter', theme: 'Log & edit repairs')] public function updateDevicev2(Request $request, $iddevices) { $user = $this->getUser(); @@ -440,6 +447,7 @@ public function updateDevicev2(Request $request, $iddevices) * ) */ + #[UserStory('As a Host, I can delete a device record from my event', persona: 'Host', theme: 'Delete devices')] public function deleteDevicev2(Request $request, $iddevices) { $user = $this->getUser(); diff --git a/app/Http/Controllers/API/DiscourseController.php b/app/Http/Controllers/API/DiscourseController.php index d8dee88ab2..071342b7f3 100644 --- a/app/Http/Controllers/API/DiscourseController.php +++ b/app/Http/Controllers/API/DiscourseController.php @@ -8,7 +8,10 @@ use Auth; use Illuminate\Http\Request; use Cache; +use App\Attributes\Feature; +use App\Attributes\UserStory; +#[Feature('Platform', description: 'Platform-wide statistics and public impact data')] class DiscourseController extends Controller { /** @@ -18,6 +21,7 @@ class DiscourseController extends Controller * @param string $tag * @return \Illuminate\Http\Response */ + #[UserStory('As a Guest, I can view recent community discussion topics', persona: 'Guest', theme: 'Discussion integration')] public function discussionTopics(Request $request, DiscourseService $discourseService, $tag = NULL) { $topics = []; diff --git a/app/Http/Controllers/API/EventController.php b/app/Http/Controllers/API/EventController.php index 38859a52d4..382807ff57 100644 --- a/app/Http/Controllers/API/EventController.php +++ b/app/Http/Controllers/API/EventController.php @@ -21,9 +21,14 @@ use Illuminate\Http\Request; use Carbon\Carbon; use Illuminate\Support\Facades\Log; +use App\Attributes\Feature; +use App\Attributes\UserStory; +use App\Attributes\NoStory; +#[Feature('Events', description: 'Community repair event management')] class EventController extends Controller { + #[UserStory('As a NetworkCoordinator, I can list events across my networks', persona: 'NetworkCoordinator', theme: 'Find & browse events')] public function getEventsByUsersNetworks(Request $request, $date_from = null, $date_to = null, $timezone = 'UTC') { $authenticatedUser = Auth::user(); @@ -145,6 +150,7 @@ public function getEventsByUsersNetworks(Request $request, $date_from = null, $d return $collection; } + #[UserStory('As a Host, I can add a volunteer to my event', persona: 'Host', theme: 'Attendance & volunteers')] public function addVolunteer(Request $request, $idevents) { $request->validate([ @@ -236,6 +242,8 @@ public function addVolunteer(Request $request, $idevents) } + #[UserStory('As a Guest, I can view confirmed volunteers for an event', persona: 'Guest', theme: 'Attendance & volunteers')] + #[UserStory('As a ThirdParty, I can retrieve volunteer data for an event via the API', persona: 'ThirdParty', theme: 'Attendance & volunteers')] public function listVolunteers(Request $request, $idevents) { $party = Party::findOrFail($idevents); @@ -287,6 +295,8 @@ public function listVolunteers(Request $request, $idevents) * ) */ + #[UserStory('As a Guest, I can view event details via the API', persona: 'Guest', theme: 'Find & browse events')] + #[UserStory('As a ThirdParty, I can retrieve event details to display on my platform', persona: 'ThirdParty', theme: 'Find & browse events')] public function getEventv2(Request $request, $idevents) { $party = Party::findOrFail($idevents); @@ -343,6 +353,8 @@ private function getUser() * ), * ) */ + #[UserStory('As a NetworkCoordinator, I can view events pending moderation in my networks', persona: 'NetworkCoordinator', theme: 'Create & manage events')] + #[UserStory('As an Admin, I can view all events pending moderation', persona: 'Admin', theme: 'Create & manage events')] public function moderateEventsv2(Request $request) { // Get the user that the API has been authenticated as. @@ -455,6 +467,7 @@ public function moderateEventsv2(Request $request) * ) * ) */ + #[UserStory('As a Host, I can create an event via the API', persona: 'Host', theme: 'Create & manage events')] public function createEventv2(Request $request) { $user = $this->getUser(); @@ -624,6 +637,7 @@ public function createEventv2(Request $request) * ) * ) */ + #[UserStory('As a Host, I can update my event via the API', persona: 'Host', theme: 'Create & manage events')] public function updateEventv2(Request $request, $idEvents) { $user = $this->getUser(); diff --git a/app/Http/Controllers/API/GroupController.php b/app/Http/Controllers/API/GroupController.php index 25076f9334..49f1d3c389 100644 --- a/app/Http/Controllers/API/GroupController.php +++ b/app/Http/Controllers/API/GroupController.php @@ -27,7 +27,11 @@ use Illuminate\Http\Request; use Notification; use Illuminate\Validation\ValidationException; +use App\Attributes\Feature; +use App\Attributes\UserStory; +use App\Attributes\NoStory; +#[Feature('Groups', description: 'Community repair group management and membership')] class GroupController extends Controller { /** @@ -38,6 +42,7 @@ class GroupController extends Controller * * Only Administrators can access this API call. */ + #[UserStory('As an Admin, I can list group audit changes for Zapier integration', persona: 'Admin', theme: 'Admin & integrations')] public static function getGroupChanges(Request $request) { $authenticatedUser = Auth::user(); @@ -60,6 +65,7 @@ public static function getGroupChanges(Request $request) return response()->json($groupChanges); } + #[UserStory('As a NetworkCoordinator, I can list all groups in my networks via the API', persona: 'NetworkCoordinator', theme: 'Network membership')] public static function getGroupsByUsersNetworks(Request $request) { $authenticatedUser = Auth::user(); @@ -180,6 +186,7 @@ public static function getGroupsByUsersNetworks(Request $request) /** * Get all of the audits related to groups from the audits table. */ + #[NoStory(reason: 'Internal audit helper for Zapier')] public static function getGroupAudits($dateFrom = null) { $query = \OwenIt\Auditing\Models\Audit::where('auditable_type', \App\Group::class); @@ -198,6 +205,7 @@ public static function getGroupAudits($dateFrom = null) * Map from the group and audit information as recorded by the audits library, * into the format needed for Zapier. */ + #[NoStory(reason: 'Internal Zapier formatting helper')] public static function mapDetailsAndAuditToChange($group, $groupAudit) { $group->makeHidden(['updated_at', 'wordpress_post_id', 'ShareableLink', 'shareable_code']); @@ -213,6 +221,7 @@ public static function mapDetailsAndAuditToChange($group, $groupAudit) return $groupChange; } + #[UserStory('As a Restarter, I can list all groups via the API', persona: 'Restarter', theme: 'Find & browse groups')] public static function getGroupList() { $groups = Group::orderBy('created_at', 'desc'); @@ -260,6 +269,8 @@ public static function getGroupList() * ) */ + #[UserStory('As a Guest, I can get a list of group names via the API', persona: 'Guest', theme: 'Find & browse groups')] + #[UserStory('As a ThirdParty, I can retrieve group names to display on my own platform', persona: 'ThirdParty', theme: 'Find & browse groups')] public static function listNamesv2(Request $request) { $request->validate([ 'includeArchived' => ['string', 'in:true,false'], @@ -311,6 +322,8 @@ public static function listNamesv2(Request $request) { * ), * ) */ + #[UserStory('As a Guest, I can get a list of group tags via the API', persona: 'Guest', theme: 'Find & browse groups')] + #[UserStory('As a ThirdParty, I can retrieve group tags to categorise groups on my platform', persona: 'ThirdParty', theme: 'Find & browse groups')] public static function listTagsv2(Request $request) { return [ 'data' => TagCollection::make(GroupTags::all()) @@ -350,6 +363,8 @@ public static function listTagsv2(Request $request) { * ), * ) */ + #[UserStory('As a Guest, I can view group details via the API', persona: 'Guest', theme: 'Find & browse groups')] + #[UserStory('As a ThirdParty, I can retrieve group details to display on my platform', persona: 'ThirdParty', theme: 'Find & browse groups')] public static function getGroupv2(Request $request, $idgroups) { $group = Group::findOrFail($idgroups); return \App\Http\Resources\Group::make($group); @@ -413,6 +428,8 @@ public static function getGroupv2(Request $request, $idgroups) { * ) */ + #[UserStory('As a Guest, I can list events for a group via the API', persona: 'Guest', theme: 'Events for group')] + #[UserStory('As a ThirdParty, I can retrieve events for a group to display on my platform', persona: 'ThirdParty', theme: 'Events for group')] public static function getEventsForGroupv2(Request $request, $idgroups) { $group = Group::findOrFail($idgroups); @@ -472,6 +489,8 @@ public static function getEventsForGroupv2(Request $request, $idgroups) { * ) */ + #[UserStory('As a Guest, I can view a group\'s volunteers via the API', persona: 'Guest', theme: 'Manage volunteers')] + #[UserStory('As a ThirdParty, I can retrieve volunteer data for a group via the API', persona: 'ThirdParty', theme: 'Manage volunteers')] public static function getVolunteersForGroupv2($idgroups) { $group = Group::findOrFail($idgroups); $volunteers = $group->allConfirmedVolunteers()->get(); @@ -514,6 +533,7 @@ public static function getVolunteersForGroupv2($idgroups) { * ) */ + #[UserStory('As a Host, I can remove a volunteer from my group', persona: 'Host', theme: 'Manage volunteers')] public function deleteVolunteerForGroupv2(Request $request, $id, $iduser) { $user = $this->getUser(); @@ -569,6 +589,7 @@ public function deleteVolunteerForGroupv2(Request $request, $id, $iduser) * ) */ + #[UserStory('As a Host, I can change a volunteer\'s role in my group', persona: 'Host', theme: 'Manage volunteers')] public function patchVolunteerForGroupv2(Request $request, $id, $iduser) { $user = $this->getUser(); @@ -644,6 +665,8 @@ private function getUser() { * ), * ) */ + #[UserStory('As a NetworkCoordinator, I can view groups pending moderation in my networks', persona: 'NetworkCoordinator', theme: 'Create & manage groups')] + #[UserStory('As an Admin, I can view all groups pending moderation', persona: 'Admin', theme: 'Create & manage groups')] public function moderateGroupsv2(Request $request) { $user = $this->getUser(); $ret = \App\Http\Resources\GroupCollection::make(Group::unapprovedVisibleTo($user->id)); @@ -726,6 +749,7 @@ public function moderateGroupsv2(Request $request) { * ) * ) */ + #[UserStory('As a Restarter, I can create a new group via the API', persona: 'Restarter', theme: 'Create & manage groups')] public function createGroupv2(Request $request) { $user = $this->getUser(); $user->convertToHost(); @@ -874,6 +898,7 @@ public function createGroupv2(Request $request) { * ) * ) */ + #[UserStory('As a Host, I can update my group via the API', persona: 'Host', theme: 'Create & manage groups')] public function updateGroupv2(Request $request, $idGroup) { $user = $this->getUser(); diff --git a/app/Http/Controllers/API/ItemController.php b/app/Http/Controllers/API/ItemController.php index a75ab527ba..a852cbf096 100644 --- a/app/Http/Controllers/API/ItemController.php +++ b/app/Http/Controllers/API/ItemController.php @@ -12,7 +12,10 @@ use Illuminate\Http\Request; use Notification; use Illuminate\Validation\ValidationException; +use App\Attributes\Feature; +use App\Attributes\UserStory; +#[Feature('Devices', description: 'Repair device tracking and impact measurement')] class ItemController extends Controller { /** @@ -38,6 +41,7 @@ class ItemController extends Controller * ), * ) */ + #[UserStory('As a Guest, I can view suggested item types for device records', persona: 'Guest', theme: 'Browse & search devices')] public static function listItemsv2(Request $request) { // Item types don't change often, so we can cache them. // Allow cache refresh for testing purposes or when running under Playwright diff --git a/app/Http/Controllers/API/NetworkController.php b/app/Http/Controllers/API/NetworkController.php index d70a0b6b18..7a30879834 100644 --- a/app/Http/Controllers/API/NetworkController.php +++ b/app/Http/Controllers/API/NetworkController.php @@ -10,9 +10,13 @@ use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use App\Attributes\Feature; +use App\Attributes\UserStory; +#[Feature('Networks', description: 'Regional network management and coordination')] class NetworkController extends Controller { + #[UserStory('As a NetworkCoordinator, I can view my network\'s statistics via the API', persona: 'NetworkCoordinator', theme: 'Network stats')] public function stats(Network $network) { if (! Auth::user()->can('view', $network)) { @@ -51,6 +55,8 @@ public function stats(Network $network) * ) */ + #[UserStory('As a Guest, I can list all networks via the API', persona: 'Guest', theme: 'Browse networks')] + #[UserStory('As a ThirdParty, I can retrieve all networks to display on my platform', persona: 'ThirdParty', theme: 'Browse networks')] public function getNetworksv2() { $networks = Network::all(); @@ -91,6 +97,8 @@ public function getNetworksv2() * ) */ + #[UserStory('As a Guest, I can view network details via the API', persona: 'Guest', theme: 'Browse networks')] + #[UserStory('As a ThirdParty, I can retrieve network details via the API', persona: 'ThirdParty', theme: 'Browse networks')] public function getNetworkv2($id) { $network = Network::findOrFail($id); @@ -189,6 +197,8 @@ public function getNetworkv2($id) * ) */ + #[UserStory('As a Guest, I can list groups for a network via the API', persona: 'Guest', theme: 'Network groups & events')] + #[UserStory('As a ThirdParty, I can retrieve groups for a network to display on my platform', persona: 'ThirdParty', theme: 'Network groups & events')] public function getNetworkGroupsv2(Request $request, $id) { $network = Network::findOrFail($id); @@ -312,6 +322,8 @@ public function getNetworkGroupsv2(Request $request, $id) * ) */ + #[UserStory('As a Guest, I can list events for a network via the API', persona: 'Guest', theme: 'Network groups & events')] + #[UserStory('As a ThirdParty, I can retrieve events for a network to display on my platform', persona: 'ThirdParty', theme: 'Network groups & events')] public function getNetworkEventsv2(Request $request, $id) { Network::findOrFail($id); diff --git a/app/Http/Controllers/API/UserController.php b/app/Http/Controllers/API/UserController.php index e27f32361c..f36e4a4fac 100644 --- a/app/Http/Controllers/API/UserController.php +++ b/app/Http/Controllers/API/UserController.php @@ -7,7 +7,11 @@ use Auth; use Illuminate\Http\Request; use Cache; +use App\Attributes\Feature; +use App\Attributes\UserStory; +use App\Attributes\NoStory; +#[Feature('Users', description: 'User accounts, profiles, and authentication')] class UserController extends Controller { /** @@ -18,6 +22,7 @@ class UserController extends Controller * * Only Administrators can access this API call. */ + #[UserStory('As an Admin, I can list user audit changes for Zapier integration', persona: 'Admin', theme: 'Data exports')] public static function changes(Request $request) { $authenticatedUser = Auth::user(); @@ -96,6 +101,7 @@ protected static function mapUserAndAuditToUserChange($user, $audit) * @param int $id * @return \Illuminate\Http\Response */ + #[NoStory(reason: 'Internal notification count endpoint')] public function notifications(Request $request, $id) { $user = User::findOrFail($id); diff --git a/app/Http/Controllers/API/UserGroupsController.php b/app/Http/Controllers/API/UserGroupsController.php index b6194192dd..44380cd4f2 100644 --- a/app/Http/Controllers/API/UserGroupsController.php +++ b/app/Http/Controllers/API/UserGroupsController.php @@ -10,7 +10,10 @@ use App\UserGroups; use Auth; use Illuminate\Http\Request; +use App\Attributes\Feature; +use App\Attributes\UserStory; +#[Feature('Groups', description: 'Community repair group management and membership')] class UserGroupsController extends Controller { /** @@ -20,6 +23,7 @@ class UserGroupsController extends Controller * * Only Administrators allowed to access this endpoint. */ + #[UserStory('As an Admin, I can list group membership changes for Zapier integration', persona: 'Admin', theme: 'Admin & integrations')] public static function changes(Request $request) { $authenticatedUser = Auth::user(); @@ -96,6 +100,7 @@ protected static function mapDetailsAndAuditToChange($userGroupAssociation, $aud * @param int $id * @return \Illuminate\Http\Response */ + #[UserStory('As a Restarter, I can leave a group I belong to', persona: 'Restarter', theme: 'Manage volunteers')] public function leave(Request $request, $id) { $authenticatedUser = Auth::user(); diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 9b581c099b..dbc2ef36a0 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -2,14 +2,19 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\NoStory; +use App\Attributes\UserStory; use App\Device; use App\Party; /** * Embedded at https://therestartproject.org/impact */ +#[Feature('Platform', description: 'Platform-wide statistics and public impact data')] class AdminController extends Controller { + #[UserStory('As a Guest, I can view the platform\'s global repair impact statistics', persona: 'Guest', theme: 'Landing page')] public static function stats($section = 1, $paragraph_only = false) { if ($section == 1) { @@ -23,6 +28,7 @@ public static function stats($section = 1, $paragraph_only = false) return view('admin.stats', $stats); } + #[NoStory(reason: 'Internal stats aggregation helper')] public static function getStats1() { $Device = new Device; @@ -95,6 +101,7 @@ public static function getStats1() ]; } + #[NoStory(reason: 'Internal stats aggregation helper')] public static function getStats2() { $stats = \App\Helpers\LcaStats::getWasteStats(); diff --git a/app/Http/Controllers/ApiController.php b/app/Http/Controllers/ApiController.php index 277105e504..f059def31e 100644 --- a/app/Http/Controllers/ApiController.php +++ b/app/Http/Controllers/ApiController.php @@ -9,6 +9,9 @@ use Auth; use DB; use Illuminate\Http\Request; +use App\Attributes\Feature; +use App\Attributes\UserStory; +use App\Attributes\NoStory; /** * @OA\Info( @@ -41,11 +44,14 @@ * name="api_token", * ) */ +#[Feature('Platform', description: 'Platform-wide statistics and public impact data')] class ApiController extends Controller { /** * Embedded at https://therestartproject.org */ + #[UserStory('As a Guest, I can view aggregate platform impact statistics', persona: 'Guest', theme: 'Platform impact stats')] + #[UserStory('As a ThirdParty, I can retrieve aggregate platform impact data for embedding', persona: 'ThirdParty', theme: 'Platform impact stats')] public static function homepage_data() { $result = []; @@ -98,6 +104,8 @@ public static function homepage_data() ->json($result, 200); } + #[UserStory('As a Guest, I can view repair statistics for a specific event', persona: 'Guest', theme: 'Platform impact stats')] + #[UserStory('As a ThirdParty, I can retrieve event repair statistics for embedding', persona: 'ThirdParty', theme: 'Platform impact stats')] public static function partyStats($partyId) { $event = Party::where('idevents', $partyId)->first(); @@ -128,6 +136,8 @@ public static function partyStats($partyId) return response()->json($result, 200); } + #[UserStory('As a Guest, I can view repair statistics for a specific group', persona: 'Guest', theme: 'Platform impact stats')] + #[UserStory('As a ThirdParty, I can retrieve group repair statistics for embedding', persona: 'ThirdParty', theme: 'Platform impact stats')] public static function groupStats($groupId) { $group = Group::where('idgroups', $groupId)->first(); @@ -159,6 +169,7 @@ public static function groupStats($groupId) return response()->json($result, 200); } + #[UserStory('As a Restarter, I can retrieve my own profile information via the API', persona: 'Restarter', theme: 'Platform impact stats')] public static function getUserInfo() { $user = Auth::user(); @@ -168,6 +179,7 @@ public static function getUserInfo() return response()->json($user->toArray()); } + #[UserStory('As an Admin, I can retrieve a list of all users via the API', persona: 'Admin', theme: 'Platform impact stats')] public static function getUserList() { $authenticatedUser = Auth::user(); @@ -188,6 +200,8 @@ public static function getUserList() * @param Request $request * @return Response */ + #[UserStory('As a Guest, I can search and filter device records via the API', persona: 'Guest', theme: 'Data exports')] + #[UserStory('As a ThirdParty, I can search and retrieve device records via the API', persona: 'ThirdParty', theme: 'Data exports')] public static function getDevices(Request $request, $page, $size) { $powered = $request->input('powered'); @@ -275,6 +289,7 @@ public static function getDevices(Request $request, $page, $size) ]); } + #[NoStory(reason: 'Timezone list helper endpoint')] public function timezones() { $zones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL_WITH_BC); $ret = []; diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 465c39ccf9..e63dcb20b7 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -2,9 +2,11 @@ namespace App\Http\Controllers\Auth; +use App\Attributes\Feature; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\SendsPasswordResetEmails; +#[Feature('Users', description: 'User accounts, profiles, and authentication')] class ForgotPasswordController extends Controller { /* diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 05f53fe768..c35884e77e 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers\Auth; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\Device; use App\Helpers\Fixometer; use App\Http\Controllers\Controller; @@ -13,6 +15,7 @@ use Illuminate\Validation\ValidationException; use Msurguy\Honeypot\Honeypot; +#[Feature('Users', description: 'User accounts, profiles, and authentication')] class LoginController extends Controller { /* @@ -53,6 +56,7 @@ public function __construct() * * @throws \Illuminate\Validation\ValidationException */ + #[UserStory('As a Guest, I can log in to the platform', persona: 'Guest', theme: 'Authentication')] public function login(Request $request) { $this->validateLogin($request); @@ -107,6 +111,7 @@ protected function validateLogin(Request $request) * * @return \Illuminate\Http\Response */ + #[UserStory('As a Guest, I can view the login page', persona: 'Guest', theme: 'Authentication')] public function showLoginForm() { $stats = Fixometer::loginRegisterStats(); diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index e70d96ae6c..95e4e0a26b 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -2,9 +2,11 @@ namespace App\Http\Controllers\Auth; +use App\Attributes\Feature; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\ResetsPasswords; +#[Feature('Users', description: 'User accounts, profiles, and authentication')] class ResetPasswordController extends Controller { /* diff --git a/app/Http/Controllers/BrandsController.php b/app/Http/Controllers/BrandsController.php index 52fb24807f..b00d95fe48 100644 --- a/app/Http/Controllers/BrandsController.php +++ b/app/Http/Controllers/BrandsController.php @@ -2,14 +2,18 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\Brands; use App\Helpers\Fixometer; use Auth; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redirect; +#[Feature('Administration', description: 'Platform administration and configuration')] class BrandsController extends Controller { + #[UserStory('As an Admin, I can view all device brands', persona: 'Admin', theme: 'Reference data')] public function index() { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -24,6 +28,7 @@ public function index() ]); } + #[UserStory('As an Admin, I can create a new device brand', persona: 'Admin', theme: 'Reference data')] public function postCreateBrand(Request $request) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -37,6 +42,7 @@ public function postCreateBrand(Request $request) return Redirect::to('brands/edit/'.$brand->id)->with('success', __('brands.create_success')); } + #[UserStory('As an Admin, I can access the form to edit a device brand', persona: 'Admin', theme: 'Reference data')] public function getEditBrand($id) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -51,6 +57,7 @@ public function getEditBrand($id) ]); } + #[UserStory('As an Admin, I can update a device brand', persona: 'Admin', theme: 'Reference data')] public function postEditBrand($id, Request $request) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -64,6 +71,7 @@ public function postEditBrand($id, Request $request) return Redirect::back()->with('success', __('brands.update_success')); } + #[UserStory('As an Admin, I can delete a device brand', persona: 'Admin', theme: 'Reference data')] public function getDeleteBrand($id) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { diff --git a/app/Http/Controllers/CalendarEventsController.php b/app/Http/Controllers/CalendarEventsController.php index 8bad356db3..774674ff75 100644 --- a/app/Http/Controllers/CalendarEventsController.php +++ b/app/Http/Controllers/CalendarEventsController.php @@ -2,6 +2,9 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\NoStory; +use App\Attributes\UserStory; use App\Group; use App\Party; use App\User; @@ -10,6 +13,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Str; +#[Feature('Events', description: 'Community repair event management')] class CalendarEventsController extends Controller { public $ical_format; @@ -19,6 +23,7 @@ public function __construct() $this->ical_format = 'Ymd\THis'; } + #[UserStory('As a Restarter, I can subscribe to my events as an iCal feed', persona: 'Restarter', theme: 'Calendar feeds')] public function allEventsByUser(Request $request, $calendar_hash) { if (empty($calendar_hash)) { @@ -53,6 +58,7 @@ public function allEventsByUser(Request $request, $calendar_hash) $this->exportCalendar($events); } + #[UserStory('As a Guest, I can subscribe to a group\'s events as an iCal feed', persona: 'Guest', theme: 'Calendar feeds')] public function allEventsByGroup(Request $request, Group $group) { $events = Party::join('groups', 'groups.idgroups', '=', 'events.group') @@ -72,6 +78,7 @@ public function allEventsByGroup(Request $request, Group $group) $this->exportCalendar($events); } + #[UserStory('As a Guest, I can subscribe to a network\'s events as an iCal feed', persona: 'Guest', theme: 'Calendar feeds')] public function allEventsByNetwork(Request $request, Network $network) { $events = Party::join('groups', 'groups.idgroups', '=', 'events.group') @@ -92,6 +99,7 @@ public function allEventsByNetwork(Request $request, Network $network) $this->exportCalendar($events); } + #[UserStory('As a Guest, I can subscribe to events in my area as an iCal feed', persona: 'Guest', theme: 'Calendar feeds')] public function allEventsByArea(Request $request, $area) { $events = Party::join('groups', 'groups.idgroups', '=', 'events.group') @@ -110,6 +118,7 @@ public function allEventsByArea(Request $request, $area) $this->exportCalendar($events); } + #[NoStory(reason: 'All-events calendar feed requiring environment secret')] public function allEvents(Request $request, $env_hash) { if ($env_hash != env('CALENDAR_HASH')) { @@ -124,6 +133,7 @@ public function allEvents(Request $request, $env_hash) $this->exportCalendar($events); } + #[NoStory(reason: 'Internal iCal generation helper')] public function exportCalendar($events) { $ical = []; diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index ee1bde1d20..3fa188351b 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\Category; use App\Helpers\Fixometer; use App\User; @@ -9,8 +11,10 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Redirect; +#[Feature('Administration', description: 'Platform administration and configuration')] class CategoryController extends Controller { + #[UserStory('As an Admin, I can view all device categories', persona: 'Admin', theme: 'Reference data')] public function index() { $Category = new Category; @@ -21,6 +25,7 @@ public function index() ]); } + #[UserStory('As an Admin, I can access the form to edit a device category', persona: 'Admin', theme: 'Reference data')] public function getEditCategory($id) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -39,6 +44,7 @@ public function getEditCategory($id) ]); } + #[UserStory('As an Admin, I can update a device category\'s details and impact factors', persona: 'Admin', theme: 'Reference data')] public function postEditCategory($id, Request $request) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index bf41ee9e39..50d653e69a 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\Group; use App\Party; use App\User; @@ -10,8 +12,10 @@ use DB; use Illuminate\Support\Facades\Log; +#[Feature('Dashboard', description: 'User dashboard with personalised event and group information')] class DashboardController extends Controller { + #[UserStory('As a Restarter, I can view my dashboard with upcoming events, my groups, and nearby groups', persona: 'Restarter', theme: 'Personal dashboard')] public function index() { $user = User::getProfile(Auth::id()); @@ -81,6 +85,7 @@ public function index() ); } + #[UserStory('As a Host, I can view the host dashboard', persona: 'Host', theme: 'Host dashboard')] public function getHostDash() { return view('dashboard.host'); diff --git a/app/Http/Controllers/DeviceController.php b/app/Http/Controllers/DeviceController.php index ae8942b466..9b1395734f 100644 --- a/app/Http/Controllers/DeviceController.php +++ b/app/Http/Controllers/DeviceController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\Brands; use App\Cluster; use App\Device; @@ -24,8 +26,10 @@ use Notification; use View; +#[Feature('Devices', description: 'Repair device tracking and impact measurement')] class DeviceController extends Controller { + #[UserStory('As a Restarter, I can browse all devices and view global repair impact data', persona: 'Restarter', theme: 'Browse & search devices')] public function index($search = null) { $user = User::getProfile(Auth::id()); @@ -66,6 +70,7 @@ public function index($search = null) ]); } + #[UserStory('As a Restarter, I can upload photos of devices I\'ve worked on', persona: 'Restarter', theme: 'Device photos')] public function imageUpload(Request $request, $id) { try { @@ -105,6 +110,7 @@ public function imageUpload(Request $request, $id) } } + #[UserStory('As a Restarter, I can delete device photos I\'ve uploaded', persona: 'Restarter', theme: 'Device photos')] public function deleteImage($device_id, $idxref) { $user = Auth::user(); diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index 358ec60db3..a13d6bd8e6 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\Device; use App\EventsUsers; use App\Group; @@ -22,16 +24,20 @@ use Response; use Illuminate\Database\Eloquent\Collection; +#[Feature('Platform', description: 'Platform-wide statistics and public impact data')] class ExportController extends Controller { + #[UserStory('As a Restarter, I can export device data from an event as CSV', persona: 'Restarter', theme: 'Data exports')] public function devicesEvent(Request $request, $idevents = NULL) { return $this->devices($request, $idevents); } + #[UserStory('As a Restarter, I can export device data from a group as CSV', persona: 'Restarter', theme: 'Data exports')] public function devicesGroup(Request $request, $idgroups = NULL) { return $this->devices($request, NULL, $idgroups); } + #[UserStory('As a Restarter, I can export all device records as CSV', persona: 'Restarter', theme: 'Data exports')] public function devices(Request $request, $idevents = NULL, $idgroups = NULL) { // To not display column if the referring URL is therestartproject.org @@ -147,6 +153,7 @@ public function devices(Request $request, $idevents = NULL, $idgroups = NULL) /** * @return \Illuminate\Http\Response */ + #[UserStory('As a Restarter, I can export a group\'s event summary as CSV', persona: 'Restarter', theme: 'Data exports')] public function groupEvents(Request $request, $idgroups) { $group = Group::findOrFail($idgroups); @@ -154,6 +161,7 @@ public function groupEvents(Request $request, $idgroups) return $this->exportEvents($parties); } + #[UserStory('As a NetworkCoordinator, I can export my network\'s event summary as CSV', persona: 'NetworkCoordinator', theme: 'Data exports')] public function networkEvents(Request $request, $id) { $network = Network::findOrFail($id); diff --git a/app/Http/Controllers/GroupController.php b/app/Http/Controllers/GroupController.php index 560def735e..5ac458b949 100644 --- a/app/Http/Controllers/GroupController.php +++ b/app/Http/Controllers/GroupController.php @@ -36,7 +36,11 @@ use Notification; use Spatie\ValidationRules\Rules\Delimited; use Carbon\Carbon; +use App\Attributes\Feature; +use App\Attributes\UserStory; +use App\Attributes\NoStory; +#[Feature('Groups', description: 'Community repair group management and membership')] class GroupController extends Controller { private function indexVariations($tab, $network) @@ -79,26 +83,31 @@ private function indexVariations($tab, $network) ]); } + #[UserStory('As a Restarter, I can browse all repair groups on the platform', persona: 'Restarter', theme: 'Find & browse groups')] public function all() { return $this->indexVariations('all', null); } + #[UserStory('As a Restarter, I can view the groups I belong to', persona: 'Restarter', theme: 'Find & browse groups')] public function mine() { return $this->indexVariations('mine', null); } + #[UserStory('As a Restarter, I can discover repair groups near my location', persona: 'Restarter', theme: 'Find & browse groups')] public function nearby() { return $this->indexVariations('nearby', null); } + #[UserStory('As a Restarter, I can browse groups within a specific network', persona: 'Restarter', theme: 'Find & browse groups')] public function network($id) { return $this->indexVariations('all', $id); } + #[UserStory('As a Restarter, I can create a new repair group and become its Host', persona: 'Restarter', theme: 'Create & manage groups')] public function create(Request $request) { $user = User::find(Auth::id()); @@ -111,6 +120,7 @@ public function create(Request $request) return view('group.create'); } + #[UserStory('As a Restarter, I can view a group\'s details, events, and members', persona: 'Restarter', theme: 'Find & browse groups')] public function view($groupid) { $user = User::find(Auth::id()); @@ -287,6 +297,7 @@ public function view($groupid) ]); } + #[UserStory('As a Host, I can send email invitations to join my group', persona: 'Host', theme: 'Group invitations')] public function postSendInvite(Request $request) { $request->validate([ @@ -379,6 +390,7 @@ public function postSendInvite(Request $request) ])); } + #[UserStory('As a Restarter, I can accept a group invitation', persona: 'Restarter', theme: 'Group invitations')] public function confirmInvite($group_id, $hash) { // Find user/group relationship based on the invitation hash. @@ -411,6 +423,7 @@ public function confirmInvite($group_id, $hash) return redirect('/group/view/'.$user_group->group)->with('success', __('groups.invite_confirmed')); } + #[UserStory('As a Host, I can edit my group\'s details and settings', persona: 'Host', theme: 'Create & manage groups')] public function edit(Request $request, $id, Geocoder $geocoder) { $user = Auth::user(); @@ -435,6 +448,7 @@ public function edit(Request $request, $id, Geocoder $geocoder) ]); } + #[UserStory('As an Admin, I can delete a group that has no device records', persona: 'Admin', theme: 'Create & manage groups')] public function delete($id) { $group = Group::where('idgroups', $id)->first(); @@ -472,6 +486,7 @@ public function delete($id) } } + #[NoStory(reason: 'Internal data expansion helper')] public static function expandGroups($groups, $your_groupids, $nearby_groupids) { $ret = []; @@ -527,6 +542,7 @@ public static function expandGroups($groups, $your_groupids, $nearby_groupids) return $ret; } + #[UserStory('As a Guest, I can view a group\'s repair impact statistics', persona: 'Guest', theme: 'Stats & data')] public static function stats($id, $format = 'row') { $group = Group::where('idgroups', $id)->first(); @@ -541,6 +557,7 @@ public static function stats($id, $format = 'row') return view('group.stats', $groupStats); } + #[UserStory('As a Restarter, I can join a repair group', persona: 'Restarter', theme: 'Manage volunteers')] public function getJoinGroup($group_id) { $user_id = Auth::id(); @@ -597,6 +614,7 @@ public function getJoinGroup($group_id) } } + #[UserStory('As a Host, I can upload an image for my group', persona: 'Host', theme: 'Photos & branding')] public function imageUpload(Request $request, $id) { try { @@ -615,6 +633,7 @@ public function imageUpload(Request $request, $id) } } + #[UserStory('As a Host, I can remove my group\'s image', persona: 'Host', theme: 'Photos & branding')] public function ajaxDeleteImage($group_id, $id, $path) { $user = Auth::user(); @@ -640,6 +659,7 @@ public function ajaxDeleteImage($group_id, $id, $path) * @param [type] $code * @return [type] */ + #[UserStory('As a Guest, I can join a group using a shareable invite code', persona: 'Guest', theme: 'Group invitations')] public function confirmCodeInvite(Request $request, $code) { // Variables diff --git a/app/Http/Controllers/GroupTagsController.php b/app/Http/Controllers/GroupTagsController.php index bc52e4446c..7a1b3ce525 100644 --- a/app/Http/Controllers/GroupTagsController.php +++ b/app/Http/Controllers/GroupTagsController.php @@ -2,14 +2,18 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\GroupTags; use App\Helpers\Fixometer; use Auth; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redirect; +#[Feature('Administration', description: 'Platform administration and configuration')] class GroupTagsController extends Controller { + #[UserStory('As an Admin, I can view all group tags', persona: 'Admin', theme: 'Reference data')] public function index() { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -24,6 +28,7 @@ public function index() ]); } + #[UserStory('As an Admin, I can create a new group tag', persona: 'Admin', theme: 'Reference data')] public function postCreateTag(Request $request) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -41,6 +46,7 @@ public function postCreateTag(Request $request) return Redirect::to('tags/edit/'.$group_tag->id)->with('success', __('group-tags.create_success')); } + #[UserStory('As an Admin, I can access the form to edit a group tag', persona: 'Admin', theme: 'Reference data')] public function getEditTag($id) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -55,6 +61,7 @@ public function getEditTag($id) ]); } + #[UserStory('As an Admin, I can update a group tag', persona: 'Admin', theme: 'Reference data')] public function postEditTag($id, Request $request) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -72,6 +79,7 @@ public function postEditTag($id, Request $request) return Redirect::back()->with('success', __('group-tags.update_success')); } + #[UserStory('As an Admin, I can delete a group tag', persona: 'Admin', theme: 'Reference data')] public function getDeleteTag($id) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index f7c66f3697..683c9ec800 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\Device; use App\Group; use App\Helpers\Fixometer; @@ -10,8 +12,10 @@ use Auth; use Illuminate\Http\Request; +#[Feature('Platform', description: 'Platform-wide statistics and public impact data')] class HomeController extends Controller { + #[UserStory('As a Guest, I can view the landing page with platform impact statistics', persona: 'Guest', theme: 'Landing page')] public function index(Request $request) { if (Auth::check()) { diff --git a/app/Http/Controllers/InformationAlertCookieController.php b/app/Http/Controllers/InformationAlertCookieController.php index f0528c2fde..841c0cf04d 100644 --- a/app/Http/Controllers/InformationAlertCookieController.php +++ b/app/Http/Controllers/InformationAlertCookieController.php @@ -2,9 +2,12 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use Cookie; use Illuminate\Http\Request; +#[Feature('Platform', description: 'Platform-wide statistics and public impact data')] class InformationAlertCookieController extends Controller { protected $minute; @@ -28,6 +31,7 @@ public function __construct() * @param int $id * @return View */ + #[UserStory('As a Guest, I can dismiss an information alert banner', persona: 'Guest', theme: 'Cookie alerts')] public function __invoke(Request $request) { if (! $request->has('dismissable_id')) { diff --git a/app/Http/Controllers/LocaleController.php b/app/Http/Controllers/LocaleController.php index f4fdfe47dc..5b3790c004 100644 --- a/app/Http/Controllers/LocaleController.php +++ b/app/Http/Controllers/LocaleController.php @@ -3,11 +3,15 @@ namespace App\Http\Controllers; use App; +use App\Attributes\Feature; +use App\Attributes\UserStory; use Auth; use LaravelLocalization; +#[Feature('Platform', description: 'Platform-wide statistics and public impact data')] class LocaleController extends Controller { + #[UserStory('As a Guest, I can switch the application language', persona: 'Guest', theme: 'Language preferences')] public function setLang($locale) { // Get local from URL and set in the session diff --git a/app/Http/Controllers/NetworkController.php b/app/Http/Controllers/NetworkController.php index a605faa874..76a92113d0 100644 --- a/app/Http/Controllers/NetworkController.php +++ b/app/Http/Controllers/NetworkController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\Group; use App\Network; use Auth; @@ -9,6 +11,7 @@ use Illuminate\Http\Request; use Lang; +#[Feature('Networks', description: 'Regional network management and coordination')] class NetworkController extends Controller { /** @@ -16,6 +19,8 @@ class NetworkController extends Controller * * @return \Illuminate\Http\Response */ + #[UserStory('As a NetworkCoordinator, I can view the networks I coordinate', persona: 'NetworkCoordinator', theme: 'Browse networks')] + #[UserStory('As an Admin, I can view all networks on the platform', persona: 'Admin', theme: 'Browse networks')] public function index() { $user = Auth::user(); @@ -46,6 +51,7 @@ public function index() * @param \App\Network $network * @return \Illuminate\Http\Response */ + #[UserStory('As a NetworkCoordinator, I can view my network\'s details and statistics', persona: 'NetworkCoordinator', theme: 'Browse networks')] public function show(Network $network) { $user = Auth::user(); @@ -70,6 +76,7 @@ public function show(Network $network) * @param \App\Network $network * @return \Illuminate\Http\Response */ + #[UserStory('As a NetworkCoordinator, I can access the form to edit my network', persona: 'NetworkCoordinator', theme: 'Manage network details')] public function edit(Network $network) { $this->authorize('update', $network); @@ -86,6 +93,7 @@ public function edit(Network $network) * @param \App\Network $network * @return \Illuminate\Http\Response */ + #[UserStory('As a NetworkCoordinator, I can update my network\'s details and logo', persona: 'NetworkCoordinator', theme: 'Manage network details')] public function update(Request $request, Network $network) { $this->authorize('update', $network); @@ -114,6 +122,7 @@ public function update(Request $request, Network $network) * @param \App\Network $network * @return \Illuminate\Http\Response */ + #[UserStory('As a NetworkCoordinator, I can add groups to my network', persona: 'NetworkCoordinator', theme: 'Network groups & events')] public function associateGroup(Request $request, Network $network) { $this->authorize('associateGroups', $network); diff --git a/app/Http/Controllers/OutboundController.php b/app/Http/Controllers/OutboundController.php index 8645fc51fe..7d24a96144 100644 --- a/app/Http/Controllers/OutboundController.php +++ b/app/Http/Controllers/OutboundController.php @@ -2,16 +2,21 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\Device; use App\Group; use App\Party; use Request; +#[Feature('Platform', description: 'Platform-wide statistics and public impact data')] class OutboundController extends Controller { /** type can be either party or group * id is id of group or party to display. * */ + #[UserStory('As a Guest, I can view embeddable CO2 impact visualisations for events and groups', persona: 'Guest', theme: 'Embeddable widgets')] + #[UserStory('As a ThirdParty, I can embed CO2 impact widgets for events and groups on my platform', persona: 'ThirdParty', theme: 'Embeddable widgets')] public static function info($type, $id, $format = 'fixometer', $return = 'view') { diff --git a/app/Http/Controllers/PartyController.php b/app/Http/Controllers/PartyController.php index 9907a31f02..abb12ba52d 100644 --- a/app/Http/Controllers/PartyController.php +++ b/app/Http/Controllers/PartyController.php @@ -2,6 +2,9 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\NoStory; +use App\Attributes\UserStory; use App\Audits; use App\Brands; use App\Cluster; @@ -34,6 +37,7 @@ use Spatie\CalendarLinks\Link; use Spatie\ValidationRules\Rules\Delimited; +#[Feature('Events', description: 'Community repair event management')] class PartyController extends Controller { protected $geocoder; @@ -45,6 +49,7 @@ public function __construct(Geocoder $geocoder, DiscourseService $discourseServi $this->discourseService = $discourseService; } + #[NoStory(reason: 'Internal data expansion helper')] public static function expandEvent($event, $group = null, $countries = null, $attending = null, $invited = null, $volunteering = null) { // Use attributesToArray rather than getAttributes so that our custom accessors are invoked. @@ -117,6 +122,7 @@ public static function expandEvent($event, $group = null, $countries = null, $at return $thisone; } + #[UserStory('As a Restarter, I can view upcoming events and events near me', persona: 'Restarter', theme: 'Find & browse events')] public function index($group_id = null) { $events = []; @@ -189,6 +195,8 @@ public function index($group_id = null) ]); } + #[UserStory('As a Host, I can access the form to create a new event for my group', persona: 'Host', theme: 'Create & manage events')] + #[UserStory('As a Host, I can create an online event without a physical location', persona: 'Host', theme: 'Create & manage events')] public function create(Request $request, $group_id = null) { $user = Auth::user(); @@ -216,6 +224,8 @@ public function create(Request $request, $group_id = null) ]); } + #[UserStory('As a Host, I can edit my group\'s event details', persona: 'Host', theme: 'Create & manage events')] + #[UserStory('As a NetworkCoordinator, I can edit events for groups in my network', persona: 'NetworkCoordinator', theme: 'Create & manage events')] public function edit($id, Request $request) { $user = Auth::user(); @@ -267,6 +277,7 @@ public function edit($id, Request $request) ]); } + #[UserStory('As a Host, I can duplicate an existing event to create a new one', persona: 'Host', theme: 'Create & manage events')] public function duplicate($id, Request $request) { $user = Auth::user(); @@ -310,6 +321,7 @@ public function duplicate($id, Request $request) ]); } + #[UserStory('As a Guest, I can view a public event\'s details and repair statistics', persona: 'Guest', theme: 'Find & browse events')] public function view($id) { $File = new FixometerFile; @@ -396,6 +408,7 @@ public function view($id) * @param object can use any Party eloquent query object * @return array either returns an array with the four links or an empty array in the rare instance when dateTime object is not created because the value is not correct */ + #[NoStory(reason: 'Calendar link helper')] public function generateAddToCalendarLinks($event) { try { @@ -414,6 +427,7 @@ public function generateAddToCalendarLinks($event) } } + #[UserStory('As a Restarter, I can RSVP to attend an upcoming event', persona: 'Restarter', theme: 'Attendance & volunteers')] public function getJoinEvent($event_id) { $user_id = Auth::id(); @@ -462,6 +476,7 @@ public function getJoinEvent($event_id) } } + #[NoStory(reason: 'Internal notification helper')] public function notifyHostsOfRsvp($user_event, $event_id) { // Get users who have appropriate role and permission to email @@ -492,6 +507,7 @@ public function notifyHostsOfRsvp($user_event, $event_id) } } + #[UserStory('As a Guest, I can view an event\'s repair impact statistics', persona: 'Guest', theme: 'Stats & data')] public static function stats($id) { $event = Party::where('idevents', $id)->first(); @@ -513,6 +529,7 @@ public static function stats($id) * * @return Response json formatted array of relevant info on users in the group. */ + #[UserStory('As a Host, I can retrieve group member emails to invite them to an event', persona: 'Host', theme: 'Invitations')] public function getGroupEmailsWithNames($event_id) { $group_user_ids = UserGroups::where('group', Party::find($event_id)->group) @@ -536,6 +553,7 @@ public function getGroupEmailsWithNames($event_id) return response()->json($group_users); } + #[UserStory('As a Host, I can update the participant count for my event', persona: 'Host', theme: 'Attendance & volunteers')] public function updateQuantity(Request $request) { $event_id = $request->input('event_id'); @@ -558,6 +576,7 @@ public function updateQuantity(Request $request) return response()->json($return); } + #[UserStory('As a Host, I can update the volunteer count for my event', persona: 'Host', theme: 'Attendance & volunteers')] public function updateVolunteerQuantity(Request $request) { $event_id = $request->input('event_id'); @@ -580,6 +599,7 @@ public function updateVolunteerQuantity(Request $request) return response()->json($return); } + #[UserStory('As a Host, I can remove a volunteer from my event', persona: 'Host', theme: 'Attendance & volunteers')] public function removeVolunteer(Request $request) { // The id that's passed in is that of the events_users table, because the entry may refer to a user without @@ -611,6 +631,7 @@ public function removeVolunteer(Request $request) } } + #[UserStory('As a Host, I can send email invitations for an event', persona: 'Host', theme: 'Invitations')] public function postSendInvite(Request $request) { $from_id = Auth::id(); @@ -710,6 +731,7 @@ public function postSendInvite(Request $request) return redirect()->back()->with('warning', __('events.invite_noemails')); } + #[UserStory('As a Restarter, I can accept an event invitation', persona: 'Restarter', theme: 'Invitations')] public function confirmInvite($event_id, $hash) { $user_event = EventsUsers::where('status', $hash)->where('event', $event_id)->first(); @@ -729,6 +751,7 @@ public function confirmInvite($event_id, $hash) return redirect('/party/view/'.intval($event_id))->with('warning', __('events.invite_invalid')); } + #[UserStory('As a Restarter, I can cancel my attendance at an event', persona: 'Restarter', theme: 'Invitations')] public function cancelInvite($event_id) { // We have to do a loop to avoid the gotcha where bulk delete operations don't invoke observers. @@ -739,6 +762,7 @@ public function cancelInvite($event_id) return redirect('/party/view/'.intval($event_id))->with('success', __('events.invite_cancelled')); } + #[UserStory('As a Restarter, I can upload photos from an event', persona: 'Restarter', theme: 'Photos & media')] public function imageUpload(Request $request, $id) { try { @@ -769,6 +793,7 @@ public function imageUpload(Request $request, $id) } } + #[UserStory('As a Restarter, I can delete my uploaded event photos', persona: 'Restarter', theme: 'Photos & media')] public function deleteImage($event_id, $id, $path) { $user = Auth::user(); @@ -790,6 +815,7 @@ public function deleteImage($event_id, $id, $path) * This sends an email to all user except the host logged in an email to ask for contributions * */ + #[UserStory('As a Host, I can request attendees log their repair contributions', persona: 'Host', theme: 'Devices & repairs')] public function getContributions($event_id) { $event = Party::find($event_id); @@ -818,6 +844,7 @@ public function getContributions($event_id) * Called via AJAX. * @param id The event id. */ + #[UserStory('As a Host, I can delete an event from my group', persona: 'Host', theme: 'Create & manage events')] public function deleteEvent($id) { $event = Party::findOrFail($id); @@ -860,6 +887,7 @@ public function deleteEvent($id) * @param [type] $code * @return [type] */ + #[UserStory('As a Guest, I can join an event using a shareable invite code', persona: 'Guest', theme: 'Invitations')] public function confirmCodeInvite(Request $request, $code) { // Variables diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php index d89aef3eec..3c03ebfcf5 100644 --- a/app/Http/Controllers/RoleController.php +++ b/app/Http/Controllers/RoleController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\Helpers\Fixometer; use App\Providers\RouteServiceProvider; use App\Role; @@ -10,9 +12,11 @@ use Auth; use Illuminate\Http\Request; +#[Feature('Administration', description: 'Platform administration and configuration')] class RoleController extends Controller { //Custom Functions + #[UserStory('As an Admin, I can view all roles and their permissions', persona: 'Admin', theme: 'Roles & permissions')] public function index() { $user = User::find(Auth::id()); @@ -33,6 +37,7 @@ public function index() return redirect(RouteServiceProvider::HOME); } + #[UserStory('As an Admin, I can edit the permissions assigned to a role', persona: 'Admin', theme: 'Roles & permissions')] public function edit($id, Request $request) { $user = Auth::user(); diff --git a/app/Http/Controllers/SkillsController.php b/app/Http/Controllers/SkillsController.php index b9b63fbb0a..414a588168 100644 --- a/app/Http/Controllers/SkillsController.php +++ b/app/Http/Controllers/SkillsController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Attributes\Feature; +use App\Attributes\UserStory; use App\Helpers\Fixometer; use App\Skills; use App\UsersSkills; @@ -9,8 +11,10 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Redirect; +#[Feature('Administration', description: 'Platform administration and configuration')] class SkillsController extends Controller { + #[UserStory('As an Admin, I can view all repair skills', persona: 'Admin', theme: 'Reference data')] public function index() { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -25,6 +29,7 @@ public function index() ]); } + #[UserStory('As an Admin, I can create a new repair skill', persona: 'Admin', theme: 'Reference data')] public function postCreateSkill(Request $request) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -39,6 +44,7 @@ public function postCreateSkill(Request $request) return Redirect::to('skills/edit/'.$skill->id)->with('success', __('skills.create_success')); } + #[UserStory('As an Admin, I can access the form to edit a repair skill', persona: 'Admin', theme: 'Reference data')] public function getEditSkill($id) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -53,6 +59,7 @@ public function getEditSkill($id) ]); } + #[UserStory('As an Admin, I can update a repair skill', persona: 'Admin', theme: 'Reference data')] public function postEditSkill($id, Request $request) { if (! Fixometer::hasRole(Auth::user(), 'Administrator')) { @@ -68,6 +75,7 @@ public function postEditSkill($id, Request $request) return Redirect::back()->with('success', __('skills.update_success')); } + #[UserStory('As an Admin, I can delete a repair skill', persona: 'Admin', theme: 'Reference data')] public function getDeleteSkill($id) { diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index d1df28ea95..c1c9e492ea 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -3,6 +3,9 @@ namespace App\Http\Controllers; use App; +use App\Attributes\Feature; +use App\Attributes\NoStory; +use App\Attributes\UserStory; use App\Device; use App\DripEvent; use App\Events\PasswordChanged; @@ -43,6 +46,7 @@ use LaravelLocalization; use Notification; +#[Feature('Users', description: 'User accounts, profiles, and authentication')] class UserController extends Controller { /** @@ -50,6 +54,7 @@ class UserController extends Controller * * @return \Illuminate\Http\Response */ + #[UserStory('As a Restarter, I can view my profile or another user\'s profile', persona: 'Restarter', theme: 'Profile management')] public function index($id = null) { if (is_null($id)) { @@ -67,6 +72,7 @@ public function index($id = null) ]); } + #[UserStory('As a Restarter, I can access the form to edit my profile', persona: 'Restarter', theme: 'Profile management')] public function getProfileEdit($id = null) { if (is_null($id)) { @@ -122,6 +128,7 @@ public function getProfileEdit($id = null) ]); } + #[UserStory('As a Restarter, I can view my notifications', persona: 'Restarter', theme: 'Notifications')] public function getNotifications() { $user = Auth::user(); @@ -133,6 +140,7 @@ public function getNotifications() ]); } + #[UserStory('As a Restarter, I can update my profile information', persona: 'Restarter', theme: 'Profile management')] public function postProfileInfoEdit(Request $request, App\Helpers\Geocoder $geocoder) { $rules = [ @@ -189,6 +197,7 @@ public function postProfileInfoEdit(Request $request, App\Helpers\Geocoder $geoc return redirect()->back()->with('message', __('profile.profile_updated')); } + #[UserStory('As a Restarter, I can change my password', persona: 'Restarter', theme: 'Profile management')] public function postProfilePasswordEdit(Request $request) { if ($request->input('id') !== null) { @@ -221,6 +230,7 @@ public function postProfilePasswordEdit(Request $request) return redirect()->back()->with('error', __('profile.password_old_mismatch')); } + #[UserStory('As an Admin, I can change a user\'s Repair Directory role', persona: 'Admin', theme: 'Admin user management')] public function postProfileRepairDirectory(Request $request) { $rules = [ @@ -249,6 +259,7 @@ public function postProfileRepairDirectory(Request $request) return redirect()->back()->with('message', __('profile.profile_updated')); } + #[UserStory('As a Restarter, I can change my preferred language', persona: 'Restarter', theme: 'Language preferences')] public function storeLanguage(Request $request) { if ($request->input('id') !== null) { @@ -275,6 +286,8 @@ public function storeLanguage(Request $request) return redirect()->back()->with('message', Lang::get('profile.language_updated')); } + #[UserStory('As a Restarter, I can delete my own account', persona: 'Restarter', theme: 'Account management')] + #[UserStory('As an Admin, I can delete a user\'s account', persona: 'Admin', theme: 'Admin user management')] public function postSoftDeleteUser(Request $request) { if ($request->input('id') !== null) { @@ -302,6 +315,7 @@ public function postSoftDeleteUser(Request $request) } } + #[UserStory('As a Restarter, I can update my notification preferences', persona: 'Restarter', theme: 'Profile management')] public function postProfilePreferencesEdit(Request $request) { if ($request->input('id') !== null) { @@ -322,6 +336,7 @@ public function postProfilePreferencesEdit(Request $request) return redirect()->back()->with('message', Lang::get('profile.preferences_updated')); } + #[UserStory('As a Restarter, I can update my repair skills', persona: 'Restarter', theme: 'Profile management')] public function postProfileTagsEdit(Request $request) { if ($request->input('id') !== null) { @@ -345,6 +360,7 @@ public function postProfileTagsEdit(Request $request) return redirect()->back()->with('message', Lang::get('profile.skills_updated')); } + #[UserStory('As a Restarter, I can upload a new profile picture', persona: 'Restarter', theme: 'Profile management')] public function postProfilePictureEdit(Request $request) { if ($request->input('id') !== null) { @@ -363,6 +379,7 @@ public function postProfilePictureEdit(Request $request) return redirect()->back()->with('error', __('profile.picture_error')); } + #[UserStory('As an Admin, I can edit a user\'s role, groups, and permissions', persona: 'Admin', theme: 'Admin user management')] public function postAdminEdit(Request $request) { if ($request->input('id') !== null) { @@ -407,6 +424,7 @@ public function postAdminEdit(Request $request) return redirect()->back()->with('message', __('profile.admin_success')); } + #[UserStory('As a Guest, I can request a password recovery email', persona: 'Guest', theme: 'Authentication')] public function recover(Request $request) { $User = new User; @@ -461,6 +479,7 @@ public function recover(Request $request) ]); } + #[UserStory('As a Guest, I can reset my password using a recovery code', persona: 'Guest', theme: 'Authentication')] public function reset(Request $request) { $User = new User; @@ -523,6 +542,7 @@ public function reset(Request $request) ]); } + #[UserStory('As an Admin, I can view and search all users on the platform', persona: 'Admin', theme: 'Admin user management')] public function all() { $user = User::find(Auth::id()); @@ -558,6 +578,7 @@ public function all() } } + #[UserStory('As an Admin, I can filter and search the user list', persona: 'Admin', theme: 'Admin user management')] public function search(Request $request) { $user = User::find(Auth::id()); @@ -629,6 +650,7 @@ public function search(Request $request) } } + #[UserStory('As an Admin, I can create a new user account', persona: 'Admin', theme: 'Admin user management')] public function create(Request $request) { $user = Auth::user(); @@ -737,6 +759,7 @@ public function create(Request $request) } } + #[UserStory('As an Admin, I can edit any user\'s account details', persona: 'Admin', theme: 'Admin user management')] public function edit($id, Request $request) { global $fixometer_languages; @@ -849,6 +872,7 @@ public function edit($id, Request $request) } } + #[UserStory('As a Restarter, I can log out of my account', persona: 'Restarter', theme: 'Authentication')] public function logout() { Auth::logout(); @@ -856,6 +880,7 @@ public function logout() return redirect('/login'); } + #[UserStory('As a Guest, I can view the registration page', persona: 'Guest', theme: 'Registration & onboarding')] public function getRegister($hash = null) { if (Auth::check() && Auth::user()->hasUserGivenConsent()) { @@ -878,6 +903,7 @@ public function getRegister($hash = null) ]); } + #[UserStory('As a Guest, I can register a new account', persona: 'Guest', theme: 'Registration & onboarding')] public function postRegister(Request $request, $hash = null) { $geocoder = new \App\Helpers\Geocoder(); @@ -1044,6 +1070,7 @@ public function postRegister(Request $request, $hash = null) } } + #[UserStory('As a Restarter, I can complete my onboarding process', persona: 'Restarter', theme: 'Registration & onboarding')] public function getOnboardingComplete() { $user = Auth::user(); @@ -1056,6 +1083,7 @@ public function getOnboardingComplete() return 'true'; } + #[NoStory(reason: 'AJAX email validation helper')] public function postEmail(Request $request) { if (User::where('email', '=', $request->get('email'))->exists()) { @@ -1063,6 +1091,7 @@ public function postEmail(Request $request) } } + #[NoStory(reason: 'MediaWiki thumbnail integration')] public static function getThumbnail(Request $request) { $user = User::where('mediawiki', $request->input('wiki_username'))->first(); @@ -1080,6 +1109,7 @@ public static function getThumbnail(Request $request) return response()->json($thumbnailPath); } + #[NoStory(reason: 'MediaWiki menu integration')] public function getUserMenus(Request $request) { $user = User::where('mediawiki', $request->input('wiki_username'))->first(); diff --git a/docs/specs/manifest.json b/docs/specs/manifest.json new file mode 100644 index 0000000000..cdc1b11715 --- /dev/null +++ b/docs/specs/manifest.json @@ -0,0 +1,3198 @@ +{ + "generatedAt": "2026-04-16T20:39:33Z", + "features": { + "Administration": { + "description": "Platform administration and configuration", + "sources": [ + "app/Http/Controllers/CategoryController.php", + "app/Http/Controllers/RoleController.php", + "app/Http/Controllers/BrandsController.php", + "app/Http/Controllers/GroupTagsController.php", + "app/Http/Controllers/API/AlertController.php", + "app/Http/Controllers/SkillsController.php" + ], + "stories": [ + { + "story": "As an Admin, I can create a platform-wide alert", + "persona": "Admin", + "theme": "Platform alerts", + "method": "AlertController::addAlertv2", + "file": "app/Http/Controllers/API/AlertController.php", + "tests": [ + { + "file": "tests/Feature/Alerts/AlertsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can update a platform alert", + "persona": "Admin", + "theme": "Platform alerts", + "method": "AlertController::updateAlertv2", + "file": "app/Http/Controllers/API/AlertController.php", + "tests": [ + { + "file": "tests/Feature/Alerts/AlertsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can delete a device brand", + "persona": "Admin", + "theme": "Reference data", + "method": "BrandsController::getDeleteBrand", + "file": "app/Http/Controllers/BrandsController.php", + "tests": [ + { + "file": "tests/Feature/Brands/BrandsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Brands/BrandsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can access the form to edit a device brand", + "persona": "Admin", + "theme": "Reference data", + "method": "BrandsController::getEditBrand", + "file": "app/Http/Controllers/BrandsController.php", + "tests": [ + { + "file": "tests/Feature/Brands/BrandsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Brands/BrandsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can view all device brands", + "persona": "Admin", + "theme": "Reference data", + "method": "BrandsController::index", + "file": "app/Http/Controllers/BrandsController.php", + "tests": [ + { + "file": "tests/Feature/Brands/BrandsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Brands/BrandsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can create a new device brand", + "persona": "Admin", + "theme": "Reference data", + "method": "BrandsController::postCreateBrand", + "file": "app/Http/Controllers/BrandsController.php", + "tests": [ + { + "file": "tests/Feature/Brands/BrandsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Brands/BrandsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can update a device brand", + "persona": "Admin", + "theme": "Reference data", + "method": "BrandsController::postEditBrand", + "file": "app/Http/Controllers/BrandsController.php", + "tests": [ + { + "file": "tests/Feature/Brands/BrandsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Brands/BrandsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can access the form to edit a device category", + "persona": "Admin", + "theme": "Reference data", + "method": "CategoryController::getEditCategory", + "file": "app/Http/Controllers/CategoryController.php", + "tests": [ + { + "file": "tests/Feature/Category/CategoryTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Category/CategoryTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can view all device categories", + "persona": "Admin", + "theme": "Reference data", + "method": "CategoryController::index", + "file": "app/Http/Controllers/CategoryController.php", + "tests": [ + { + "file": "tests/Feature/Category/CategoryTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can update a device category's details and impact factors", + "persona": "Admin", + "theme": "Reference data", + "method": "CategoryController::postEditCategory", + "file": "app/Http/Controllers/CategoryController.php", + "tests": [ + { + "file": "tests/Feature/Category/CategoryTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Category/CategoryTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can delete a group tag", + "persona": "Admin", + "theme": "Reference data", + "method": "GroupTagsController::getDeleteTag", + "file": "app/Http/Controllers/GroupTagsController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupTagsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can access the form to edit a group tag", + "persona": "Admin", + "theme": "Reference data", + "method": "GroupTagsController::getEditTag", + "file": "app/Http/Controllers/GroupTagsController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupTagsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can view all group tags", + "persona": "Admin", + "theme": "Reference data", + "method": "GroupTagsController::index", + "file": "app/Http/Controllers/GroupTagsController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupTagsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can create a new group tag", + "persona": "Admin", + "theme": "Reference data", + "method": "GroupTagsController::postCreateTag", + "file": "app/Http/Controllers/GroupTagsController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupTagsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can update a group tag", + "persona": "Admin", + "theme": "Reference data", + "method": "GroupTagsController::postEditTag", + "file": "app/Http/Controllers/GroupTagsController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupTagsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can edit the permissions assigned to a role", + "persona": "Admin", + "theme": "Roles & permissions", + "method": "RoleController::edit", + "file": "app/Http/Controllers/RoleController.php", + "tests": [ + { + "file": "tests/Feature/Role/RoleTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can view all roles and their permissions", + "persona": "Admin", + "theme": "Roles & permissions", + "method": "RoleController::index", + "file": "app/Http/Controllers/RoleController.php", + "tests": [ + { + "file": "tests/Feature/Role/RoleTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Role/RoleTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Role/RoleTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can delete a repair skill", + "persona": "Admin", + "theme": "Reference data", + "method": "SkillsController::getDeleteSkill", + "file": "app/Http/Controllers/SkillsController.php", + "tests": [ + { + "file": "tests/Feature/Users/SkillsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can access the form to edit a repair skill", + "persona": "Admin", + "theme": "Reference data", + "method": "SkillsController::getEditSkill", + "file": "app/Http/Controllers/SkillsController.php", + "tests": [ + { + "file": "tests/Feature/Users/SkillsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can view all repair skills", + "persona": "Admin", + "theme": "Reference data", + "method": "SkillsController::index", + "file": "app/Http/Controllers/SkillsController.php", + "tests": [ + { + "file": "tests/Feature/Users/SkillsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Users/SkillsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Users/SkillsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can create a new repair skill", + "persona": "Admin", + "theme": "Reference data", + "method": "SkillsController::postCreateSkill", + "file": "app/Http/Controllers/SkillsController.php", + "tests": [ + { + "file": "tests/Feature/Users/SkillsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can update a repair skill", + "persona": "Admin", + "theme": "Reference data", + "method": "SkillsController::postEditSkill", + "file": "app/Http/Controllers/SkillsController.php", + "tests": [ + { + "file": "tests/Feature/Users/SkillsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view active platform alerts", + "persona": "Guest", + "theme": "Platform alerts", + "method": "AlertController::listAlertsv2", + "file": "app/Http/Controllers/API/AlertController.php", + "tests": [ + { + "file": "tests/Feature/Alerts/AlertsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Alerts/AlertsTest.php", + "test": "(unknown test)" + } + ] + } + ], + "storyCount": 23, + "personas": [ + "Admin", + "Guest" + ] + }, + "Dashboard": { + "description": "User dashboard with personalised event and group information", + "sources": [ + "app/Http/Controllers/DashboardController.php" + ], + "stories": [ + { + "story": "As a Host, I can view the host dashboard", + "persona": "Host", + "theme": "Host dashboard", + "method": "DashboardController::getHostDash", + "file": "app/Http/Controllers/DashboardController.php", + "tests": [] + }, + { + "story": "As a Restarter, I can view my dashboard with upcoming events, my groups, and nearby groups", + "persona": "Restarter", + "theme": "Personal dashboard", + "method": "DashboardController::index", + "file": "app/Http/Controllers/DashboardController.php", + "tests": [ + { + "file": "tests/Feature/Dashboard/BasicTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Dashboard/BasicTest.php", + "test": "(unknown test)" + } + ] + } + ], + "storyCount": 2, + "personas": [ + "Host", + "Restarter" + ] + }, + "Devices": { + "description": "Repair device tracking and impact measurement", + "sources": [ + "app/Http/Controllers/API/ItemController.php", + "app/Http/Controllers/API/DeviceController.php", + "app/Http/Controllers/DeviceController.php" + ], + "stories": [ + { + "story": "As a Guest, I can view device details via the API", + "persona": "Guest", + "theme": "Get device details", + "method": "DeviceController::getDevicev2", + "file": "app/Http/Controllers/API/DeviceController.php", + "tests": [ + { + "file": "tests/Feature/Devices/APIv2DeviceTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/APIv2DeviceTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view suggested item types for device records", + "persona": "Guest", + "theme": "Browse & search devices", + "method": "ItemController::listItemsv2", + "file": "app/Http/Controllers/API/ItemController.php", + "tests": [ + { + "file": "tests/Feature/Devices/CategoryTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can delete a device record from my event", + "persona": "Host", + "theme": "Delete devices", + "method": "DeviceController::deleteDevicev2", + "file": "app/Http/Controllers/API/DeviceController.php", + "tests": [ + { + "file": "tests/Feature/Devices/EditTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can log a device repair at an event I attended", + "persona": "Restarter", + "theme": "Log & edit repairs", + "method": "DeviceController::createDevicev2", + "file": "app/Http/Controllers/API/DeviceController.php", + "tests": [ + { + "file": "tests/Feature/Devices/NullEstimateProblemTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/SparePartsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/SparePartsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/SparePartsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/SparePartsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/SparePartsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/NullProblemTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/NullAgeProblemTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/TooManyMiscTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/CategoryTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/APIv2DeviceTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/EditTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/EditTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can delete device photos I've uploaded", + "persona": "Restarter", + "theme": "Device photos", + "method": "DeviceController::deleteImage", + "file": "app/Http/Controllers/DeviceController.php", + "tests": [ + { + "file": "tests/Feature/Devices/EditTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/EditTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can upload photos of devices I've worked on", + "persona": "Restarter", + "theme": "Device photos", + "method": "DeviceController::imageUpload", + "file": "app/Http/Controllers/DeviceController.php", + "tests": [ + { + "file": "tests/Feature/Devices/EditTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/EditTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can browse all devices and view global repair impact data", + "persona": "Restarter", + "theme": "Browse & search devices", + "method": "DeviceController::index", + "file": "app/Http/Controllers/DeviceController.php", + "tests": [ + { + "file": "tests/Feature/Fixometer/BasicTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can update a device repair record at an event I attended", + "persona": "Restarter", + "theme": "Log & edit repairs", + "method": "DeviceController::updateDevicev2", + "file": "app/Http/Controllers/API/DeviceController.php", + "tests": [ + { + "file": "tests/Feature/Devices/NullEstimateProblemTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/NullAgeProblemTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/CategoryTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/EditTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/EditTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/EditTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve device repair data via the API", + "persona": "ThirdParty", + "theme": "Get device details", + "method": "DeviceController::getDevicev2", + "file": "app/Http/Controllers/API/DeviceController.php", + "tests": [ + { + "file": "tests/Feature/Devices/APIv2DeviceTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Devices/APIv2DeviceTest.php", + "test": "(unknown test)" + } + ] + } + ], + "storyCount": 9, + "personas": [ + "Guest", + "Host", + "Restarter", + "ThirdParty" + ] + }, + "Events": { + "description": "Community repair event management", + "sources": [ + "app/Http/Controllers/PartyController.php", + "app/Http/Controllers/API/EventController.php", + "app/Http/Controllers/CalendarEventsController.php" + ], + "stories": [ + { + "story": "As an Admin, I can view all events pending moderation", + "persona": "Admin", + "theme": "Create & manage events", + "method": "EventController::moderateEventsv2", + "file": "app/Http/Controllers/API/EventController.php", + "tests": [ + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can subscribe to events in my area as an iCal feed", + "persona": "Guest", + "theme": "Calendar feeds", + "method": "CalendarEventsController::allEventsByArea", + "file": "app/Http/Controllers/CalendarEventsController.php", + "tests": [ + { + "file": "tests/Feature/Calendar/CalendarTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can subscribe to a group's events as an iCal feed", + "persona": "Guest", + "theme": "Calendar feeds", + "method": "CalendarEventsController::allEventsByGroup", + "file": "app/Http/Controllers/CalendarEventsController.php", + "tests": [ + { + "file": "tests/Feature/Calendar/CalendarTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can subscribe to a network's events as an iCal feed", + "persona": "Guest", + "theme": "Calendar feeds", + "method": "CalendarEventsController::allEventsByNetwork", + "file": "app/Http/Controllers/CalendarEventsController.php", + "tests": [ + { + "file": "tests/Feature/Calendar/CalendarTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view event details via the API", + "persona": "Guest", + "theme": "Find & browse events", + "method": "EventController::getEventv2", + "file": "app/Http/Controllers/API/EventController.php", + "tests": [ + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view confirmed volunteers for an event", + "persona": "Guest", + "theme": "Attendance & volunteers", + "method": "EventController::listVolunteers", + "file": "app/Http/Controllers/API/EventController.php", + "tests": [ + { + "file": "tests/Feature/Events/AddRemoveVolunteerTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can join an event using a shareable invite code", + "persona": "Guest", + "theme": "Invitations", + "method": "PartyController::confirmCodeInvite", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view an event's repair impact statistics", + "persona": "Guest", + "theme": "Stats & data", + "method": "PartyController::stats", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Stats/EventStatsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view a public event's details and repair statistics", + "persona": "Guest", + "theme": "Find & browse events", + "method": "PartyController::view", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/AddRemoveVolunteerTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/JoinEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Stats/EventStatsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Unit/Events/EventStateTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Unit/CharsetTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can add a volunteer to my event", + "persona": "Host", + "theme": "Attendance & volunteers", + "method": "EventController::addVolunteer", + "file": "app/Http/Controllers/API/EventController.php", + "tests": [ + { + "file": "tests/Feature/Events/AddRemoveVolunteerTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can create an event via the API", + "persona": "Host", + "theme": "Create & manage events", + "method": "EventController::createEventv2", + "file": "app/Http/Controllers/API/EventController.php", + "tests": [ + { + "file": "tests/Feature/Events/OnlineEventsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Unit/Events/EventStateTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Unit/Events/TimezoneTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Unit/Events/TimezoneTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can update my event via the API", + "persona": "Host", + "theme": "Create & manage events", + "method": "EventController::updateEventv2", + "file": "app/Http/Controllers/API/EventController.php", + "tests": [ + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Dashboard/BasicTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Unit/Events/TimezoneTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can access the form to create a new event for my group", + "persona": "Host", + "theme": "Create & manage events", + "method": "PartyController::create", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can create an online event without a physical location", + "persona": "Host", + "theme": "Create & manage events", + "method": "PartyController::create", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can delete an event from my group", + "persona": "Host", + "theme": "Create & manage events", + "method": "PartyController::deleteEvent", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can duplicate an existing event to create a new one", + "persona": "Host", + "theme": "Create & manage events", + "method": "PartyController::duplicate", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can edit my group's event details", + "persona": "Host", + "theme": "Create & manage events", + "method": "PartyController::edit", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can request attendees log their repair contributions", + "persona": "Host", + "theme": "Devices & repairs", + "method": "PartyController::getContributions", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/EventRequestReviewEmailTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can retrieve group member emails to invite them to an event", + "persona": "Host", + "theme": "Invitations", + "method": "PartyController::getGroupEmailsWithNames", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can send email invitations for an event", + "persona": "Host", + "theme": "Invitations", + "method": "PartyController::postSendInvite", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/AddRemoveVolunteerTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can remove a volunteer from my event", + "persona": "Host", + "theme": "Attendance & volunteers", + "method": "PartyController::removeVolunteer", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/AddRemoveVolunteerTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can update the participant count for my event", + "persona": "Host", + "theme": "Attendance & volunteers", + "method": "PartyController::updateQuantity", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/AttendanceTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can update the volunteer count for my event", + "persona": "Host", + "theme": "Attendance & volunteers", + "method": "PartyController::updateVolunteerQuantity", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/AttendanceTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can list events across my networks", + "persona": "NetworkCoordinator", + "theme": "Find & browse events", + "method": "EventController::getEventsByUsersNetworks", + "file": "app/Http/Controllers/API/EventController.php", + "tests": [ + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can view events pending moderation in my networks", + "persona": "NetworkCoordinator", + "theme": "Create & manage events", + "method": "EventController::moderateEventsv2", + "file": "app/Http/Controllers/API/EventController.php", + "tests": [ + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can edit events for groups in my network", + "persona": "NetworkCoordinator", + "theme": "Create & manage events", + "method": "PartyController::edit", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can subscribe to my events as an iCal feed", + "persona": "Restarter", + "theme": "Calendar feeds", + "method": "CalendarEventsController::allEventsByUser", + "file": "app/Http/Controllers/CalendarEventsController.php", + "tests": [ + { + "file": "tests/Feature/Calendar/CalendarTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Calendar/CalendarTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Calendar/CalendarTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Calendar/CalendarTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Calendar/CalendarTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can cancel my attendance at an event", + "persona": "Restarter", + "theme": "Invitations", + "method": "PartyController::cancelInvite", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/JoinEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can accept an event invitation", + "persona": "Restarter", + "theme": "Invitations", + "method": "PartyController::confirmInvite", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can delete my uploaded event photos", + "persona": "Restarter", + "theme": "Photos & media", + "method": "PartyController::deleteImage", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/ModerationEventPhotosNotificationTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can RSVP to attend an upcoming event", + "persona": "Restarter", + "theme": "Attendance & volunteers", + "method": "PartyController::getJoinEvent", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/JoinEventTest.php", + "test": "PHPUnit" + }, + { + "file": "tests/Feature/Events/JoinEventTest.php", + "test": "PHPUnit" + } + ] + }, + { + "story": "As a Restarter, I can upload photos from an event", + "persona": "Restarter", + "theme": "Photos & media", + "method": "PartyController::imageUpload", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/ModerationEventPhotosNotificationTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can view upcoming events and events near me", + "persona": "Restarter", + "theme": "Find & browse events", + "method": "PartyController::index", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/Events/InviteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Dashboard/BasicTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Unit/Events/TimezoneTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve event details to display on my platform", + "persona": "ThirdParty", + "theme": "Find & browse events", + "method": "EventController::getEventv2", + "file": "app/Http/Controllers/API/EventController.php", + "tests": [ + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve volunteer data for an event via the API", + "persona": "ThirdParty", + "theme": "Attendance & volunteers", + "method": "EventController::listVolunteers", + "file": "app/Http/Controllers/API/EventController.php", + "tests": [ + { + "file": "tests/Feature/Events/AddRemoveVolunteerTest.php", + "test": "(unknown test)" + } + ] + } + ], + "storyCount": 35, + "personas": [ + "Admin", + "Guest", + "Host", + "NetworkCoordinator", + "Restarter", + "ThirdParty" + ] + }, + "Groups": { + "description": "Community repair group management and membership", + "sources": [ + "app/Http/Controllers/API/UserGroupsController.php", + "app/Http/Controllers/API/GroupController.php", + "app/Http/Controllers/GroupController.php" + ], + "stories": [ + { + "story": "As an Admin, I can delete a group that has no device records", + "persona": "Admin", + "theme": "Create & manage groups", + "method": "GroupController::delete", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupDeleteTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupDeleteTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupDeleteTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupDeleteTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can list group audit changes for Zapier integration", + "persona": "Admin", + "theme": "Admin & integrations", + "method": "GroupController::getGroupChanges", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Zapier/ZapierNetworkTests.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Zapier/ZapierNetworkTests.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can view all groups pending moderation", + "persona": "Admin", + "theme": "Create & manage groups", + "method": "GroupController::moderateGroupsv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can list group membership changes for Zapier integration", + "persona": "Admin", + "theme": "Admin & integrations", + "method": "UserGroupsController::changes", + "file": "app/Http/Controllers/API/UserGroupsController.php", + "tests": [ + { + "file": "tests/Feature/Zapier/ZapierNetworkTests.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Zapier/ZapierNetworkTests.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can join a group using a shareable invite code", + "persona": "Guest", + "theme": "Group invitations", + "method": "GroupController::confirmCodeInvite", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/InviteGroupTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can list events for a group via the API", + "persona": "Guest", + "theme": "Events for group", + "method": "GroupController::getEventsForGroupv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "PHPUnit" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "PHPUnit" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view group details via the API", + "persona": "Guest", + "theme": "Find & browse groups", + "method": "GroupController::getGroupv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view a group's volunteers via the API", + "persona": "Guest", + "theme": "Manage volunteers", + "method": "GroupController::getVolunteersForGroupv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupHostTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can get a list of group names via the API", + "persona": "Guest", + "theme": "Find & browse groups", + "method": "GroupController::listNamesv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can get a list of group tags via the API", + "persona": "Guest", + "theme": "Find & browse groups", + "method": "GroupController::listTagsv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view a group's repair impact statistics", + "persona": "Guest", + "theme": "Stats & data", + "method": "GroupController::stats", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Stats/GroupStatsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can remove my group's image", + "persona": "Host", + "theme": "Photos & branding", + "method": "GroupController::ajaxDeleteImage", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupEditTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can remove a volunteer from my group", + "persona": "Host", + "theme": "Manage volunteers", + "method": "GroupController::deleteVolunteerForGroupv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupHostTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupHostTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can edit my group's details and settings", + "persona": "Host", + "theme": "Create & manage groups", + "method": "GroupController::edit", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupEditTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupEditTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupEditTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupEditTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can upload an image for my group", + "persona": "Host", + "theme": "Photos & branding", + "method": "GroupController::imageUpload", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupEditTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can change a volunteer's role in my group", + "persona": "Host", + "theme": "Manage volunteers", + "method": "GroupController::patchVolunteerForGroupv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupHostTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupHostTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupHostTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can send email invitations to join my group", + "persona": "Host", + "theme": "Group invitations", + "method": "GroupController::postSendInvite", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/InviteGroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/InviteGroupTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Host, I can update my group via the API", + "persona": "Host", + "theme": "Create & manage groups", + "method": "GroupController::updateGroupv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupCreateTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupEditTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupEditTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupEditTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Users/GroupsNearbyTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Unit/Events/TimezoneTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can list all groups in my networks via the API", + "persona": "NetworkCoordinator", + "theme": "Network membership", + "method": "GroupController::getGroupsByUsersNetworks", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can view groups pending moderation in my networks", + "persona": "NetworkCoordinator", + "theme": "Create & manage groups", + "method": "GroupController::moderateGroupsv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can browse all repair groups on the platform", + "persona": "Restarter", + "theme": "Find & browse groups", + "method": "GroupController::all", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/BasicTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can accept a group invitation", + "persona": "Restarter", + "theme": "Group invitations", + "method": "GroupController::confirmInvite", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/InviteGroupTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can create a new repair group and become its Host", + "persona": "Restarter", + "theme": "Create & manage groups", + "method": "GroupController::create", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupCreateTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupCreateTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupCreateTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupCreateTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can create a new group via the API", + "persona": "Restarter", + "theme": "Create & manage groups", + "method": "GroupController::createGroupv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Zapier/ZapierNetworkTests.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Zapier/ZapierNetworkTests.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupNetworkCreateTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can list all groups via the API", + "persona": "Restarter", + "theme": "Find & browse groups", + "method": "GroupController::getGroupList", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupCreateTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can join a repair group", + "persona": "Restarter", + "theme": "Manage volunteers", + "method": "GroupController::getJoinGroup", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupJoinTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can view the groups I belong to", + "persona": "Restarter", + "theme": "Find & browse groups", + "method": "GroupController::mine", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/BasicTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can discover repair groups near my location", + "persona": "Restarter", + "theme": "Find & browse groups", + "method": "GroupController::nearby", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/BasicTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can browse groups within a specific network", + "persona": "Restarter", + "theme": "Find & browse groups", + "method": "GroupController::network", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can view a group's details, events, and members", + "persona": "Restarter", + "theme": "Find & browse groups", + "method": "GroupController::view", + "file": "app/Http/Controllers/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Stats/EventStatsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/InviteGroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/InviteGroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupViewTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupViewTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupViewTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupViewTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupViewTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can leave a group I belong to", + "persona": "Restarter", + "theme": "Manage volunteers", + "method": "UserGroupsController::leave", + "file": "app/Http/Controllers/API/UserGroupsController.php", + "tests": [ + { + "file": "tests/Feature/Events/AddRemoveVolunteerTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupJoinTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve events for a group to display on my platform", + "persona": "ThirdParty", + "theme": "Events for group", + "method": "GroupController::getEventsForGroupv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "PHPUnit" + }, + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "PHPUnit" + }, + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve group details to display on my platform", + "persona": "ThirdParty", + "theme": "Find & browse groups", + "method": "GroupController::getGroupv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve volunteer data for a group via the API", + "persona": "ThirdParty", + "theme": "Manage volunteers", + "method": "GroupController::getVolunteersForGroupv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Events/CreateEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupHostTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve group names to display on my own platform", + "persona": "ThirdParty", + "theme": "Find & browse groups", + "method": "GroupController::listNamesv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve group tags to categorise groups on my platform", + "persona": "ThirdParty", + "theme": "Find & browse groups", + "method": "GroupController::listTagsv2", + "file": "app/Http/Controllers/API/GroupController.php", + "tests": [ + { + "file": "tests/Feature/Groups/APIv2GroupTest.php", + "test": "(unknown test)" + } + ] + } + ], + "storyCount": 36, + "personas": [ + "Admin", + "Guest", + "Host", + "NetworkCoordinator", + "Restarter", + "ThirdParty" + ] + }, + "Networks": { + "description": "Regional network management and coordination", + "sources": [ + "app/Http/Controllers/NetworkController.php", + "app/Http/Controllers/API/NetworkController.php" + ], + "stories": [ + { + "story": "As an Admin, I can view all networks on the platform", + "persona": "Admin", + "theme": "Browse networks", + "method": "NetworkController::index", + "file": "app/Http/Controllers/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can list events for a network via the API", + "persona": "Guest", + "theme": "Network groups & events", + "method": "NetworkController::getNetworkEventsv2", + "file": "app/Http/Controllers/API/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Networks/APIv2NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can list groups for a network via the API", + "persona": "Guest", + "theme": "Network groups & events", + "method": "NetworkController::getNetworkGroupsv2", + "file": "app/Http/Controllers/API/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/APIv2NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can list all networks via the API", + "persona": "Guest", + "theme": "Browse networks", + "method": "NetworkController::getNetworksv2", + "file": "app/Http/Controllers/API/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/APIv2NetworkTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Networks/APIv2NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view network details via the API", + "persona": "Guest", + "theme": "Browse networks", + "method": "NetworkController::getNetworkv2", + "file": "app/Http/Controllers/API/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/APIv2NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can add groups to my network", + "persona": "NetworkCoordinator", + "theme": "Network groups & events", + "method": "NetworkController::associateGroup", + "file": "app/Http/Controllers/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can access the form to edit my network", + "persona": "NetworkCoordinator", + "theme": "Manage network details", + "method": "NetworkController::edit", + "file": "app/Http/Controllers/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can view the networks I coordinate", + "persona": "NetworkCoordinator", + "theme": "Browse networks", + "method": "NetworkController::index", + "file": "app/Http/Controllers/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can view my network's details and statistics", + "persona": "NetworkCoordinator", + "theme": "Browse networks", + "method": "NetworkController::show", + "file": "app/Http/Controllers/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can view my network's statistics via the API", + "persona": "NetworkCoordinator", + "theme": "Network stats", + "method": "NetworkController::stats", + "file": "app/Http/Controllers/API/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can update my network's details and logo", + "persona": "NetworkCoordinator", + "theme": "Manage network details", + "method": "NetworkController::update", + "file": "app/Http/Controllers/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve events for a network to display on my platform", + "persona": "ThirdParty", + "theme": "Network groups & events", + "method": "NetworkController::getNetworkEventsv2", + "file": "app/Http/Controllers/API/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Events/APIv2EventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Networks/APIv2NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve groups for a network to display on my platform", + "persona": "ThirdParty", + "theme": "Network groups & events", + "method": "NetworkController::getNetworkGroupsv2", + "file": "app/Http/Controllers/API/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/APIv2NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve all networks to display on my platform", + "persona": "ThirdParty", + "theme": "Browse networks", + "method": "NetworkController::getNetworksv2", + "file": "app/Http/Controllers/API/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/APIv2NetworkTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Networks/APIv2NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve network details via the API", + "persona": "ThirdParty", + "theme": "Browse networks", + "method": "NetworkController::getNetworkv2", + "file": "app/Http/Controllers/API/NetworkController.php", + "tests": [ + { + "file": "tests/Feature/Networks/APIv2NetworkTest.php", + "test": "(unknown test)" + } + ] + } + ], + "storyCount": 15, + "personas": [ + "Admin", + "Guest", + "NetworkCoordinator", + "ThirdParty" + ] + }, + "Platform": { + "description": "Platform-wide statistics and public impact data", + "sources": [ + "app/Http/Controllers/OutboundController.php", + "app/Http/Controllers/InformationAlertCookieController.php", + "app/Http/Controllers/ApiController.php", + "app/Http/Controllers/API/DiscourseController.php", + "app/Http/Controllers/HomeController.php", + "app/Http/Controllers/LocaleController.php", + "app/Http/Controllers/ExportController.php", + "app/Http/Controllers/AdminController.php" + ], + "stories": [ + { + "story": "As an Admin, I can retrieve a list of all users via the API", + "persona": "Admin", + "theme": "Platform impact stats", + "method": "ApiController::getUserList", + "file": "app/Http/Controllers/ApiController.php", + "tests": [ + { + "file": "tests/Feature/Users/ProfileTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view the platform's global repair impact statistics", + "persona": "Guest", + "theme": "Landing page", + "method": "AdminController::stats", + "file": "app/Http/Controllers/AdminController.php", + "tests": [ + { + "file": "tests/Feature/Stats/EventStatsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can search and filter device records via the API", + "persona": "Guest", + "theme": "Data exports", + "method": "ApiController::getDevices", + "file": "app/Http/Controllers/ApiController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupViewTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view repair statistics for a specific group", + "persona": "Guest", + "theme": "Platform impact stats", + "method": "ApiController::groupStats", + "file": "app/Http/Controllers/ApiController.php", + "tests": [ + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Stats/GroupStatsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view aggregate platform impact statistics", + "persona": "Guest", + "theme": "Platform impact stats", + "method": "ApiController::homepage_data", + "file": "app/Http/Controllers/ApiController.php", + "tests": [] + }, + { + "story": "As a Guest, I can view repair statistics for a specific event", + "persona": "Guest", + "theme": "Platform impact stats", + "method": "ApiController::partyStats", + "file": "app/Http/Controllers/ApiController.php", + "tests": [ + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view recent community discussion topics", + "persona": "Guest", + "theme": "Discussion integration", + "method": "DiscourseController::discussionTopics", + "file": "app/Http/Controllers/API/DiscourseController.php", + "tests": [ + { + "file": "tests/Feature/Dashboard/BasicTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view the landing page with platform impact statistics", + "persona": "Guest", + "theme": "Landing page", + "method": "HomeController::index", + "file": "app/Http/Controllers/HomeController.php", + "tests": [ + { + "file": "tests/Feature/Home/HomeTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Home/HomeTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can dismiss an information alert banner", + "persona": "Guest", + "theme": "Cookie alerts", + "method": "InformationAlertCookieController::__invoke", + "file": "app/Http/Controllers/InformationAlertCookieController.php", + "tests": [] + }, + { + "story": "As a Guest, I can switch the application language", + "persona": "Guest", + "theme": "Language preferences", + "method": "LocaleController::setLang", + "file": "app/Http/Controllers/LocaleController.php", + "tests": [ + { + "file": "tests/Feature/Users/EditLanguageSettingsTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Dashboard/LanguageSwitcherTest.php", + "test": "PHPUnit" + } + ] + }, + { + "story": "As a Guest, I can view embeddable CO2 impact visualisations for events and groups", + "persona": "Guest", + "theme": "Embeddable widgets", + "method": "OutboundController::info", + "file": "app/Http/Controllers/OutboundController.php", + "tests": [ + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupViewTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a NetworkCoordinator, I can export my network's event summary as CSV", + "persona": "NetworkCoordinator", + "theme": "Data exports", + "method": "ExportController::networkEvents", + "file": "app/Http/Controllers/ExportController.php", + "tests": [ + { + "file": "tests/Feature/Events/ExportTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can retrieve my own profile information via the API", + "persona": "Restarter", + "theme": "Platform impact stats", + "method": "ApiController::getUserInfo", + "file": "app/Http/Controllers/ApiController.php", + "tests": [ + { + "file": "tests/Feature/Users/ProfileTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can export all device records as CSV", + "persona": "Restarter", + "theme": "Data exports", + "method": "ExportController::devices", + "file": "app/Http/Controllers/ExportController.php", + "tests": [ + { + "file": "tests/Feature/Events/ExportTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Fixometer/BasicTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can export device data from an event as CSV", + "persona": "Restarter", + "theme": "Data exports", + "method": "ExportController::devicesEvent", + "file": "app/Http/Controllers/ExportController.php", + "tests": [ + { + "file": "tests/Feature/Events/ExportTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/ExportTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can export device data from a group as CSV", + "persona": "Restarter", + "theme": "Data exports", + "method": "ExportController::devicesGroup", + "file": "app/Http/Controllers/ExportController.php", + "tests": [ + { + "file": "tests/Feature/Events/ExportTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Events/ExportTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can export a group's event summary as CSV", + "persona": "Restarter", + "theme": "Data exports", + "method": "ExportController::groupEvents", + "file": "app/Http/Controllers/ExportController.php", + "tests": [ + { + "file": "tests/Feature/Events/ExportTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can search and retrieve device records via the API", + "persona": "ThirdParty", + "theme": "Data exports", + "method": "ApiController::getDevices", + "file": "app/Http/Controllers/ApiController.php", + "tests": [ + { + "file": "tests/Feature/Groups/GroupViewTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve group repair statistics for embedding", + "persona": "ThirdParty", + "theme": "Platform impact stats", + "method": "ApiController::groupStats", + "file": "app/Http/Controllers/ApiController.php", + "tests": [ + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Stats/GroupStatsTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can retrieve aggregate platform impact data for embedding", + "persona": "ThirdParty", + "theme": "Platform impact stats", + "method": "ApiController::homepage_data", + "file": "app/Http/Controllers/ApiController.php", + "tests": [] + }, + { + "story": "As a ThirdParty, I can retrieve event repair statistics for embedding", + "persona": "ThirdParty", + "theme": "Platform impact stats", + "method": "ApiController::partyStats", + "file": "app/Http/Controllers/ApiController.php", + "tests": [ + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a ThirdParty, I can embed CO2 impact widgets for events and groups on my platform", + "persona": "ThirdParty", + "theme": "Embeddable widgets", + "method": "OutboundController::info", + "file": "app/Http/Controllers/OutboundController.php", + "tests": [ + { + "file": "tests/Feature/Events/DeleteEventTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Groups/GroupViewTest.php", + "test": "(unknown test)" + } + ] + } + ], + "storyCount": 22, + "personas": [ + "Admin", + "Guest", + "NetworkCoordinator", + "Restarter", + "ThirdParty" + ] + }, + "Users": { + "description": "User accounts, profiles, and authentication", + "sources": [ + "app/Http/Controllers/UserController.php", + "app/Http/Controllers/Auth/LoginController.php", + "app/Http/Controllers/API/UserController.php" + ], + "stories": [ + { + "story": "As an Admin, I can view and search all users on the platform", + "persona": "Admin", + "theme": "Admin user management", + "method": "UserController::all", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/UserAdminTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Admin/Users/ViewUsersTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Admin/Users/ViewUsersTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Admin/Users/ViewUsersTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can list user audit changes for Zapier integration", + "persona": "Admin", + "theme": "Data exports", + "method": "UserController::changes", + "file": "app/Http/Controllers/API/UserController.php", + "tests": [ + { + "file": "tests/Feature/Zapier/ZapierNetworkTests.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Zapier/ZapierNetworkTests.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can create a new user account", + "persona": "Admin", + "theme": "Admin user management", + "method": "UserController::create", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/Registration/AccountCreationTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can edit any user's account details", + "persona": "Admin", + "theme": "Admin user management", + "method": "UserController::edit", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Events/AddRemoveVolunteerTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Users/ProfileTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Users/ProfileTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can edit a user's role, groups, and permissions", + "persona": "Admin", + "theme": "Admin user management", + "method": "UserController::postAdminEdit", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Events/AddRemoveVolunteerTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Networks/NetworkTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can change a user's Repair Directory role", + "persona": "Admin", + "theme": "Admin user management", + "method": "UserController::postProfileRepairDirectory", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/ProfileTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can delete a user's account", + "persona": "Admin", + "theme": "Admin user management", + "method": "UserController::postSoftDeleteUser", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/UserAdminTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As an Admin, I can filter and search the user list", + "persona": "Admin", + "theme": "Admin user management", + "method": "UserController::search", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Admin/Users/ViewUsersTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Admin/Users/ViewUsersTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can log in to the platform", + "persona": "Guest", + "theme": "Authentication", + "method": "LoginController::login", + "file": "app/Http/Controllers/Auth/LoginController.php", + "tests": [ + { + "file": "tests/Feature/Users/RecoverTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Admin/Users/WikiLoginTests.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Admin/Users/WikiLoginTests.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Admin/Users/WikiLoginTests.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Admin/Users/WikiLoginTests.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view the login page", + "persona": "Guest", + "theme": "Authentication", + "method": "LoginController::showLoginForm", + "file": "app/Http/Controllers/Auth/LoginController.php", + "tests": [ + { + "file": "tests/Feature/Users/RecoverTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can view the registration page", + "persona": "Guest", + "theme": "Registration & onboarding", + "method": "UserController::getRegister", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/Registration/AccountCreationTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can register a new account", + "persona": "Guest", + "theme": "Registration & onboarding", + "method": "UserController::postRegister", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/Registration/AccountCreationTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can request a password recovery email", + "persona": "Guest", + "theme": "Authentication", + "method": "UserController::recover", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/PasswordResetTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Users/PasswordResetTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Users/PasswordResetTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Users/RecoverTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Guest, I can reset my password using a recovery code", + "persona": "Guest", + "theme": "Authentication", + "method": "UserController::reset", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/PasswordResetTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Users/RecoverTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can view my notifications", + "persona": "Restarter", + "theme": "Notifications", + "method": "UserController::getNotifications", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Notifications/BasicTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can complete my onboarding process", + "persona": "Restarter", + "theme": "Registration & onboarding", + "method": "UserController::getOnboardingComplete", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/Registration/AccountCreationTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can access the form to edit my profile", + "persona": "Restarter", + "theme": "Profile management", + "method": "UserController::getProfileEdit", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/EditProfileTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can view my profile or another user's profile", + "persona": "Restarter", + "theme": "Profile management", + "method": "UserController::index", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/ProfileTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can log out of my account", + "persona": "Restarter", + "theme": "Authentication", + "method": "UserController::logout", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/Registration/AccountCreationTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can update my profile information", + "persona": "Restarter", + "theme": "Profile management", + "method": "UserController::postProfileInfoEdit", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/EditProfileTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Users/EditProfileTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can change my password", + "persona": "Restarter", + "theme": "Profile management", + "method": "UserController::postProfilePasswordEdit", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/ProfileTest.php", + "test": "(unknown test)" + }, + { + "file": "tests/Feature/Admin/Users/WikiLoginTests.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can upload a new profile picture", + "persona": "Restarter", + "theme": "Profile management", + "method": "UserController::postProfilePictureEdit", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/EditProfileTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can update my notification preferences", + "persona": "Restarter", + "theme": "Profile management", + "method": "UserController::postProfilePreferencesEdit", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/ProfileTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can update my repair skills", + "persona": "Restarter", + "theme": "Profile management", + "method": "UserController::postProfileTagsEdit", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/EditProfileTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can delete my own account", + "persona": "Restarter", + "theme": "Account management", + "method": "UserController::postSoftDeleteUser", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/UserAdminTest.php", + "test": "(unknown test)" + } + ] + }, + { + "story": "As a Restarter, I can change my preferred language", + "persona": "Restarter", + "theme": "Language preferences", + "method": "UserController::storeLanguage", + "file": "app/Http/Controllers/UserController.php", + "tests": [ + { + "file": "tests/Feature/Users/ProfileTest.php", + "test": "(unknown test)" + } + ] + } + ], + "storyCount": 26, + "personas": [ + "Admin", + "Guest", + "Restarter" + ] + } + }, + "personas": { + "Admin": { + "features": [ + "Administration", + "Events", + "Groups", + "Networks", + "Platform", + "Users" + ], + "storyCount": 37 + }, + "Guest": { + "features": [ + "Administration", + "Devices", + "Events", + "Groups", + "Networks", + "Platform", + "Users" + ], + "storyCount": 38 + }, + "Host": { + "features": [ + "Dashboard", + "Devices", + "Events", + "Groups" + ], + "storyCount": 23 + }, + "NetworkCoordinator": { + "features": [ + "Events", + "Groups", + "Networks", + "Platform" + ], + "storyCount": 12 + }, + "Restarter": { + "features": [ + "Dashboard", + "Devices", + "Events", + "Groups", + "Platform", + "Users" + ], + "storyCount": 41 + }, + "ThirdParty": { + "features": [ + "Devices", + "Events", + "Groups", + "Networks", + "Platform" + ], + "storyCount": 17 + } + }, + "coverage": { + "annotatedStories": 168, + "storiesWithTests": 164 + } +} diff --git a/docs/specs/narratives/administration.md b/docs/specs/narratives/administration.md new file mode 100644 index 0000000000..165710ac57 --- /dev/null +++ b/docs/specs/narratives/administration.md @@ -0,0 +1,19 @@ + +# Administration + +Platform administration covers the configuration and management of the system's reference data and settings. This includes device categories (with their environmental impact factors), device brands, repair skills, group tags, user roles and permissions, and platform-wide alerts. + +## What Admins can do + +Admins manage all reference data for the platform: + +- **Categories** -- View, edit, and configure device categories with CO2 footprints and weight factors that drive impact calculations +- **Brands** -- Create, edit, and delete device brands used in repair logging +- **Skills** -- Create, edit, and delete repair skills that volunteers can add to their profiles +- **Group Tags** -- Create, edit, and delete tags used to categorise groups +- **Roles** -- View all roles and edit the permissions assigned to each +- **Alerts** -- Create and update platform-wide alert banners shown to all users + +## What Guests can do + +Guests can view currently active platform alerts. diff --git a/docs/specs/narratives/dashboard.md b/docs/specs/narratives/dashboard.md new file mode 100644 index 0000000000..a0c9613017 --- /dev/null +++ b/docs/specs/narratives/dashboard.md @@ -0,0 +1,12 @@ + +# Dashboard + +The dashboard is the authenticated user's home page, providing a personalised overview of their repair community activity. + +## What Restarters can do + +Restarters see a dashboard with upcoming events they're attending, their groups, nearby groups, and recently created groups in their area. The dashboard also surfaces recent discussion topics from Discourse (Talk). + +## What Hosts can do + +Hosts have access to a dedicated host dashboard view with management-focused information about their groups and events. diff --git a/docs/specs/narratives/devices.md b/docs/specs/narratives/devices.md new file mode 100644 index 0000000000..8c520f7152 --- /dev/null +++ b/docs/specs/narratives/devices.md @@ -0,0 +1,20 @@ + +# Devices + +Devices represent items brought to repair events. Each device record captures what was brought, its category, brand, repair status (fixed, repairable, end-of-life), and the environmental impact prevented. This data powers the platform's impact statistics -- waste diverted and CO2 emissions prevented. + +## What Restarters can do + +Restarters who attended an event can log device repairs, update existing records, upload photos of devices they worked on, and delete their uploaded photos. They can also browse all devices and view global repair impact data on the Fixometer page. + +## What Hosts can do + +Hosts can delete device records from their events and perform any device action that Restarters can for events belonging to their group. + +## What Guests can do + +Guests can view device details and browse suggested item types via the public API. + +## What ThirdParties can do + +External organisations can retrieve device repair data via the public API for analysis or display on their own platforms. diff --git a/docs/specs/narratives/events.md b/docs/specs/narratives/events.md new file mode 100644 index 0000000000..0e9a8a3099 --- /dev/null +++ b/docs/specs/narratives/events.md @@ -0,0 +1,28 @@ + +# Events + +Community repair events are the core activity of Restarters. Groups organise events where volunteers come together to repair broken items brought in by the public. Events can be physical (at a venue) or online. + +## What Hosts can do + +Hosts manage the full lifecycle of events for their groups. They create events with a date, time, and location (or mark them as online), edit details, duplicate past events as templates, and delete events when needed. Hosts invite volunteers by email, manage RSVPs, update participant and volunteer counts, and record walk-in attendees. After an event, Hosts can request that attendees log the devices they repaired. + +## What NetworkCoordinators can do + +NetworkCoordinators have oversight of events across all groups in their network. They can edit events, moderate events pending approval, and list events across their network via the API. + +## What Admins can do + +Admins can moderate all events pending approval across the entire platform and perform any action a Host or NetworkCoordinator can. + +## What Restarters can do + +Restarters browse upcoming events, view events near them, RSVP to attend, accept or cancel invitations, and upload photos from events they attended. They can also subscribe to event calendars via iCal feeds. + +## What Guests can do + +Guests (unauthenticated visitors) can view public event details and repair impact statistics, subscribe to iCal calendar feeds for groups, networks, or geographic areas, and join events using shareable invite codes. + +## What ThirdParties can do + +External organisations can retrieve event details, volunteer data, and event listings for networks and groups via the public API to display on their own platforms. diff --git a/docs/specs/narratives/groups.md b/docs/specs/narratives/groups.md new file mode 100644 index 0000000000..9570c4e7dd --- /dev/null +++ b/docs/specs/narratives/groups.md @@ -0,0 +1,28 @@ + +# Groups + +Repair groups are the organisational unit of Restarters. Each group represents a local community of repair volunteers who come together to run events. Groups have a name, location, description, and can belong to one or more networks. + +## What Hosts can do + +Hosts manage their group's details, settings, and membership. They edit group information, upload images, send email invitations to join, manage volunteer roles (promoting members to Host or demoting them), and remove volunteers. Hosts can also update their group via the API. + +## What NetworkCoordinators can do + +NetworkCoordinators oversee groups within their networks. They can list all groups in their networks, view groups pending moderation, approve groups, and manage group-network associations. They can also remove volunteers and change roles for groups in their network. + +## What Admins can do + +Admins can view all groups pending moderation across the platform, delete groups (only if they have no device records), and list group audit changes for Zapier integration. + +## What Restarters can do + +Restarters can browse all groups, view groups they belong to, discover nearby groups, browse groups within a network, and join or leave groups. Creating a new group automatically promotes a Restarter to Host. Restarters can also accept group invitations and list groups via the API. + +## What Guests can do + +Guests can view group details, repair statistics, volunteer lists, events, and tags via the public API. They can also join groups using shareable invite codes. + +## What ThirdParties can do + +External organisations can retrieve group names, tags, details, events, and volunteer data via the public API to display on their own platforms. diff --git a/docs/specs/narratives/networks.md b/docs/specs/narratives/networks.md new file mode 100644 index 0000000000..619bf98834 --- /dev/null +++ b/docs/specs/narratives/networks.md @@ -0,0 +1,20 @@ + +# Networks + +Networks are regional organisations that groups can belong to. They provide a layer of coordination and oversight above individual groups -- for example, a national repair network might coordinate dozens of local repair groups. + +## What NetworkCoordinators can do + +NetworkCoordinators manage their assigned networks. They can view their networks, see network details and statistics, edit network information and upload logos, and add groups to their network. They can also view network statistics via the API. + +## What Admins can do + +Admins can view all networks on the platform and perform any action a NetworkCoordinator can across all networks. + +## What Guests can do + +Guests can list all networks, view network details, and browse a network's groups and events via the public API. + +## What ThirdParties can do + +External organisations can retrieve network listings, details, groups, and events via the public API to display on their own platforms. diff --git a/docs/specs/narratives/platform.md b/docs/specs/narratives/platform.md new file mode 100644 index 0000000000..015f046433 --- /dev/null +++ b/docs/specs/narratives/platform.md @@ -0,0 +1,24 @@ + +# Platform + +Platform-wide features that serve the public face of Restarters -- the landing page, global impact statistics, data exports, embeddable widgets, and integration points used by external sites. + +## What Guests can do + +Guests can view the landing page with platform impact statistics, switch the application language, dismiss alert banners, view recent community discussion topics from Discourse, and access embeddable CO2 impact visualisations for events and groups. They can also view aggregate statistics, search and filter device records, and view per-event and per-group repair statistics via the API. + +## What Restarters can do + +Restarters can export device data from events, groups, or the entire platform as CSV files. They can also export event summaries for groups and retrieve their profile information via the API. + +## What NetworkCoordinators can do + +NetworkCoordinators can export event summaries for their networks as CSV files. + +## What Admins can do + +Admins can retrieve a list of all users via the API. + +## What ThirdParties can do + +External organisations can retrieve aggregate platform impact data, event and group repair statistics, search device records, and embed CO2 impact widgets on their own platforms. diff --git a/docs/specs/narratives/users.md b/docs/specs/narratives/users.md new file mode 100644 index 0000000000..3fb0a1654f --- /dev/null +++ b/docs/specs/narratives/users.md @@ -0,0 +1,16 @@ + +# Users + +Users are the people who participate in the Restarters community. The platform supports four roles: Admin (full oversight), Host (manages groups and events), Restarter (attends events and logs repairs), and NetworkCoordinator (regional oversight). Users authenticate via a single sign-on system that spans Restarters, Discourse (Talk), and MediaWiki (Wiki). + +## What Guests can do + +Guests can view the registration page, register a new account, log in, request password recovery, and reset their password. + +## What Restarters can do + +Restarters manage their own profile -- updating personal information, changing their password, uploading a profile picture, setting their preferred language, and managing notification preferences and repair skills. They can view other users' profiles, view their notifications, complete onboarding, and delete their own account. They can also log out and retrieve their profile via the API. + +## What Admins can do + +Admins have full user management capabilities. They can view and search all users, create new accounts, edit any user's details (including role, groups, and permissions), change Repair Directory roles, and delete user accounts. Admin changes are tracked for Zapier integration. diff --git a/docs/superpowers/specs/2026-04-16-living-specs-design.md b/docs/superpowers/specs/2026-04-16-living-specs-design.md new file mode 100644 index 0000000000..4bc49f395f --- /dev/null +++ b/docs/superpowers/specs/2026-04-16-living-specs-design.md @@ -0,0 +1,423 @@ +# Living Specifications: Browsable Feature & Persona Documentation + +## Problem + +Restarters.net has extensive functionality across events, groups, devices, users, and networks, with multiple personas (Host, Admin, Restarter, NetworkCoordinator). There is no single place where a human can understand what the system does, organised by feature area or by persona. Test suites verify behaviour but don't communicate it at a readable level. User stories exist in Jira but aren't connected to the code. CLAUDE.md describes conventions, not capabilities. + +## Solution + +Embed structured annotations in PHP code that declare what each method enables and for whom. Extract these into a manifest. Use Claude to generate human-readable narrative summaries per feature area. Build a browsable static site with dual navigation (by feature, by persona) and deploy to GitHub Pages. + +## Design Decisions + +- **Source of truth:** PHP 8 attributes in the code, not external files or Jira +- **Maintenance model:** Claude updates annotations automatically during development +- **Narrative layer:** AI-generated markdown files, committed to the repo, human-editable +- **Browsable output:** VitePress static site on GitHub Pages, built via GitHub Actions +- **Jira integration:** None. This is a standalone documentation system +- **Validation:** CI warnings for drift, with option to tighten to hard-fail later +- **Test linking:** Tests reference user stories via `@story:ClassName::method` annotations; coverage shown on the site + +## 1. PHP Attributes + +Three attribute classes in `app/Attributes/`: + +### Feature + +Marks a class as belonging to a feature area. + +```php +#[Attribute(Attribute::TARGET_CLASS)] +class Feature +{ + public function __construct( + public string $name, + public string $description = '', + ) {} +} +``` + +Usage: + +```php +#[Feature('Events', description: 'Community repair event management')] +class PartyController extends Controller { ... } +``` + +### UserStory + +Marks a method with what it enables and for which persona. + +```php +#[Attribute(Attribute::TARGET_METHOD)] +class UserStory +{ + public function __construct( + public string $story, + public string $persona, + public string $feature = '', + ) {} +} +``` + +Usage: + +```php +#[UserStory( + 'As a Host, I can create a new repair event for my group', + persona: 'Host', + feature: 'Events' +)] +public function create(Request $request) { ... } +``` + +The `feature` parameter is optional — if omitted, the feature is inherited from the class-level `#[Feature]` attribute. + +### NoStory + +Explicitly marks a method as intentionally unannotated. + +```php +#[Attribute(Attribute::TARGET_METHOD)] +class NoStory +{ + public function __construct( + public string $reason = '', + ) {} +} +``` + +Usage: + +```php +#[NoStory(reason: 'Internal middleware hook')] +public function middleware() { ... } +``` + +## 2. Extraction Pipeline + +An artisan command `php artisan specs:extract` that: + +1. Scans all PHP files under `app/` using `nikic/php-parser` to find `#[Feature]` and `#[UserStory]` attributes +2. Builds a structured manifest +3. Scans test files under `tests/` and `resources/js/` for `@story:` references and matches them to stories +4. Writes `docs/specs/manifest.json` + +### Manifest Format + +```json +{ + "generatedAt": "2026-04-16T12:00:00Z", + "features": { + "Events": { + "description": "Community repair event management", + "sources": ["app/Http/Controllers/PartyController.php"], + "stories": [ + { + "story": "As a Host, I can create a new repair event for my group", + "persona": "Host", + "method": "PartyController::create", + "file": "app/Http/Controllers/PartyController.php", + "tests": [ + { + "file": "tests/Feature/EventTest.php", + "test": "test_host_can_create_event" + }, + { + "file": "tests/playwright/events.spec.ts", + "test": "Host can create event" + } + ] + } + ], + "storyCount": 16, + "personas": ["Host", "Admin", "Restarter"] + } + }, + "personas": { + "Host": { + "features": ["Events", "Groups"], + "storyCount": 24 + }, + "Admin": { + "features": ["Events", "Groups", "Users", "Networks"], + "storyCount": 31 + } + }, + "coverage": { + "annotatedMethods": 87, + "noStoryMethods": 12, + "unannotatedMethods": 23 + } +} +``` + +The manifest is deterministic (same code always produces the same output) and committed to the repo. The site build only needs Node.js, not PHP. + +### Static parsing, not reflection + +The command uses `nikic/php-parser` for static analysis rather than PHP reflection. This means it does not need to boot the Laravel application, load the database, or resolve dependencies. It is fast and safe to run in CI. + +## 3. AI-Generated Narratives + +Narrative summary files at `docs/specs/narratives/{feature}.md`. These are the "how Events work" layer that makes the documentation readable. + +### Format + +```markdown + +# Events + +Community repair events are the core activity of Restarters. Groups +organise events where volunteers repair broken items brought in by +the public. + +## What Hosts can do + +Hosts manage the full lifecycle of events for their groups -- creating +events with a date, time and location, editing details, inviting +volunteers, and recording the devices brought for repair. + +## What Admins can do + +Admins have oversight across all events -- they can moderate, delete, +or reassign events across any group. + +## What Restarters can do + +Restarters can browse upcoming events, RSVP to attend, and log the +devices they've repaired. +``` + +### Maintenance rules + +- Claude generates these during development, committed alongside code changes +- Organised by feature, with persona subsections +- Human-written prose is preserved -- Claude adds/removes persona sections and updates story counts but does not rewrite paragraphs a human has edited +- The `specs:hash` comment tracks the story count for staleness detection + +## 4. Static Site + +A VitePress site in `specs-site/` that consumes `manifest.json` and narrative markdown files. + +### Navigation + +Two switchable views: + +**Feature view** (default): + +``` +Events + Overview (narrative) + Host stories (8) + Admin stories (3) + Restarter stories (5) +Groups + Overview (narrative) + Host stories (12) + NetworkCoordinator stories (6) +``` + +**Persona view** (toggle): + +``` +Host + Events (8 stories) + Groups (12 stories) + Devices (4 stories) +Admin + Events (3 stories) + Groups (5 stories) + Users (7 stories) +``` + +### Story display + +Each user story entry shows: +- The story text +- The method it's attached to (e.g., `PartyController::create`) +- A link to the source file on GitHub +- Test coverage indicator: **Covered** (at least one test), **Multi-layer** (PHPUnit + Playwright), or **Uncovered** + +Feature and persona overview pages show aggregate coverage (e.g., "Events: 14/16 stories covered (87%)"). + +### Build process + +A prebuild script reads `manifest.json` and narratives, generates VitePress-compatible markdown pages for each feature and persona view, then VitePress builds the HTML. + +### File structure + +``` +specs-site/ + .vitepress/ + config.ts + theme/ + index.md + features/ + personas/ + package.json +``` + +## 5. Deployment + +### Quick preview (development / demos) + +For showing the site to someone quickly during development: + +```bash +cd specs-site +npm run build +npx surge dist/ my-restarters-specs.surge.sh +``` + +Surge gives a public URL instantly with no setup. Use this for feature branch previews and demos. + +### Production (GitHub Pages) + +A workflow at `.github/workflows/specs-site.yml`: + +1. Triggers on push to `develop` +2. Checks out the repo +3. Installs Node.js dependencies for `specs-site/` +4. Runs the prebuild script (generates pages from manifest + narratives) +5. Builds VitePress +6. Deploys to GitHub Pages + +No PHP needed in CI -- the manifest is already committed. + +## 6. CI Validation + +A separate CI step (can be in the same workflow or the existing test workflow): + +### Manifest drift detection + +Runs `php artisan specs:extract` and compares output against committed `manifest.json`. Fails if they differ. + +### Orphan detection (warnings) + +- Public methods on `#[Feature]` classes that have neither `#[UserStory]` nor `#[NoStory]` -- warns "PartyController::update has no story" +- Narrative files referencing personas or story counts that don't match the manifest + +### Narrative staleness (warnings) + +Compares the `specs:hash` comment in each narrative file against the current manifest. If story count has changed, warns "Events narrative may be stale (was 16 stories, now 18)." + +### PR comment + +When annotations change, the action posts a PR comment: "This PR modifies 3 user stories in Events. [View changes →]" + +### Test coverage warnings + +Stories with no `@story:` reference in any test file generate a warning: "UserStory PartyController::create has no test coverage." Not a hard failure -- some stories may be tested indirectly. + +### Future tightening + +The orphan detection can be promoted from warning to hard-fail for controller classes specifically. This is a configuration flag in the extraction command, not a code change. + +## 7. Test Coverage Linking + +Tests reference user stories via a `@story:` annotation pointing to the annotated method. + +### PHPUnit + +```php +/** + * @story PartyController::create + */ +public function test_host_can_create_event(): void +{ + // ... +} +``` + +### Playwright + +```js +test('Host can create event @story:PartyController::create', async ({ page }) => { + // ... +}); +``` + +### Jest + +```js +test('Host can create event @story:PartyController::create', () => { + // ... +}); +``` + +### How it works + +The `specs:extract` command scans test files for `@story:ClassName::method` patterns and matches them to user stories in the manifest. Each story gains a `tests` array listing the test file and test name. + +The browsable site shows coverage at every level: +- Per story: covered / uncovered / multi-layer indicator +- Per feature: "Events: 14/16 stories covered (87%)" +- Per persona: "Host: 20/24 stories covered (83%)" + +## 8. CLAUDE.md Integration + +Add to the project CLAUDE.md: + +```markdown +## Living Specifications + +When modifying PHP controller or service methods: +- Maintain `#[UserStory]` and `#[Feature]` attributes (in `app/Attributes/`) +- Add `#[UserStory]` to new public methods that represent user-facing functionality +- Add `#[NoStory]` to methods that intentionally have no user story +- Update the story text if you change what a method does +- When adding or modifying tests, include `@story:ClassName::method` references +- Run `php artisan specs:extract` after annotation changes and commit the updated manifest +- Update the narrative in `docs/specs/narratives/` if feature coverage has changed +- Preserve human-written prose in narratives -- update structure and counts, not wording +``` + +## 9. File Structure Summary + +``` +app/ + Attributes/ + Feature.php + UserStory.php + NoStory.php + Console/Commands/ + SpecsExtract.php + +docs/ + specs/ + manifest.json + narratives/ + events.md + groups.md + devices.md + users.md + networks.md + +specs-site/ + .vitepress/ + config.ts + theme/ + index.md + features/ + personas/ + package.json + +.github/ + workflows/ + specs-site.yml +``` + +## Personas (known) + +From the existing codebase: +- **Admin** -- full platform oversight +- **Host** -- manages events and groups +- **Restarter** -- attends events, logs repairs +- **NetworkCoordinator** -- regional oversight across groups in a network + +Additional personas will emerge naturally from the annotations. + +## Open Questions + +None -- all design decisions have been made. Ready for implementation planning. diff --git a/features/01-Overview/1-Vision.feature b/features/01-Overview/1-Vision.feature deleted file mode 100644 index d11a9cbee6..0000000000 --- a/features/01-Overview/1-Vision.feature +++ /dev/null @@ -1,15 +0,0 @@ -Feature: 1 Vision - -The Fixometer is a data collection and visualisation tool that lets repair -organisations easily and conveniently collect information on repairs, and -visualise the impact of those repairs. - -The Fixometer fits into the Restart Project's vision of changing the world's -relationship with electronics, by providing a platform for the collection of -open repair data. It was the initial phase of our development of the open repair -data standard, which will allow for open repair data to be collected and pooled -together from multiple organisations using different tools. The Fixometer -continues to be developed as the flagship and archetype tool for open repair -data collection. - -For further information, see https://therestartproject.org/fixometer. \ No newline at end of file diff --git a/features/01-Overview/2-Goals.feature b/features/01-Overview/2-Goals.feature deleted file mode 100644 index 57a6640f45..0000000000 --- a/features/01-Overview/2-Goals.feature +++ /dev/null @@ -1,5 +0,0 @@ -Feature: 2 Goals - -* Increase quantity and quality of repair data that is collected electronically. -* Enthuse repair volunteers and activists around the world - diff --git a/features/01-Overview/3- Capabilities.feature b/features/01-Overview/3- Capabilities.feature deleted file mode 100644 index 1b9a269aae..0000000000 --- a/features/01-Overview/3- Capabilities.feature +++ /dev/null @@ -1,31 +0,0 @@ -Feature: 3 Capabitilies - -* Ability to record Restart volunteers within the system - * With corresponding data capture on: - * Name (could be a pseudonym) - * Location - * Email - * Age (within ranges) - * Gender (?) -* Ability for self-registration/onboarding of volunteers -* Profile completion -* Opt-in to the uses of the system -* Ability to record time volunteered by volunteers, by linking Restarters to events -* Ability to report on time volunteered -* Filtered by age and location -* Ability to tag groups for reporting purposes - -* To recognise service - for Hosts and Restarters to see time volunteered -* To have a record of who was in attendance in the case of any liability issues -* To potentially send event reports to Restarters -* To allow Restarters edit access to device data, to improve it -* To allow for invite and private RSVP for scheduled events -* To allow for export of time-related data in a way that would help time-bankers -* To understand and monitor patterns in Restarter participation, based on region, age and other data -* To integrate with our onboarding of volunteers globally – to a communication platform, and to the Wiki -* To reinforce best practice, safety and community values -* To allow for open feedback on a given event – a bit like a graffiti wall for volunteers, that is a safe space -* To allow Restarters opportunity to get credit for device fixes and get a history -* To allow unaffiliated Restarters to sign up - detect areas with numerous volunteers -* Help hosts find restarters in their area -* To allow for photos of devices by Restarters \ No newline at end of file diff --git a/features/01-Overview/4-Glossary.feature b/features/01-Overview/4-Glossary.feature deleted file mode 100644 index 48013ce7fd..0000000000 --- a/features/01-Overview/4-Glossary.feature +++ /dev/null @@ -1,110 +0,0 @@ -Feature: 4 Glossary - -Event (AKA Restart Party) -: Free community electronics repair event. People join us to fix their broken -electronics. At events, Restart Party Hosts log the status of devices, and -afterwards feed into our online database. - -Participant AKA Owner -: A participant is someone who brings a device to an event for repair. -Who comes to events? People who are frustrated with our throwaway culture, -people who cannot afford to buy new, people who are curious to get inside -their black-box gadgets. In our experience, an equal number of men and women -bring gadgets for repair, and participants come from all walks of life. - -Device (AKA item) -: Devices are brought to events in order to be fixed. -What is a device? A wide range of items from laptops through to toasters. -Devices are categorised into device categories. - -Device categories. -: There are 34 device categories. - -Category cluster. -: Devices categories are split into four clusters: computers and home office, -electronic gadgets, home entertainment, kitchen and household items. - -Repairers AKA Restarters. -: We call our volunteer repairers "Restarters". They are talented amateurs, -with various backgrounds and experience. Owners get involved in the repair, -helping troubleshoot, disassemble and sometimes fix their own electronics. - -Calculations -: For each category we maintain data on the average weight; data on how -repaired devices displace new devices (which is where the environmental -benefit comes in); and data on the carbon footprint to manufacture each type -of electrical device. - -Representative products -: A product which we deem to be representative of a particular category. - -Average weight -: We used the Furniture Reuse Network’s 2009 data as a starting point, updated -some figures, and sourced more online, as averages of representative products. - -Displacement rate -: The rate by which we prevent a new manufacture. Our biggest – and most -necessary – assumption is that a fix at a Restart Party displaces a new -manufacture by 50%. That is, that a repaired device will live on for an extra -50% more than its intended life. - -Carbon footprint -: The amount of carbon required to manufacture each type of electrical device. - -Data quality indicators -: Data quality indicators are used in carbon footprinting to ensure that the -accuracy of each datapoint is understood and transparent. - -Repairability -: Many devices leave a Restart Party that seem worthy of more effort. -Some need spare parts that we don’t have at our immediate disposal. -Sometimes device owners go home ready to finish the job themselves – this -occurs in about 4.6% of cases. Others are referred to professional repairers. -These are recorded as “repairable”. - -End-of-life -: A device is labelled "end-of-life" when a Restarter and the owner decide it -is not cost-effective or realistic to repair the device. - -Reuse opportunity -: We encourage owners of end-of-life devices to seek out a reuse opportunity -before taking them to be recycled. This is the most resource-efficient option. -Many devices can be sold or given away for parts. But ultimately, we would like -to see devices recycled so that their materials can be recovered. - -Types of fixes -: There can be software fixes or hardware fixes. -Many of our fixes are software related. Issues related to software contribute -to a feeling of "perceived obsolescence", and motivate an owner of a device to -abandon it, when a simple software fix can address the frustrations of a user -and prolong the lifecycle of a gadget. In fact, often times mobiles, tablets -and laptops can refuse to boot due to software problems. With laptops, -software fixes are just as common as hardware ones. - -Fix frequency -: The total amount of times particular devices are fixed at our events. -Devices that are *brought* frequently may have a high fix frequency. -Laptops, mobiles, and small kitchen items are brought frequently and as a -result have a high fix frequency. - -Fix rates -: Different from the fix frequency, this is the ratio that devices brought to -event ends up being fixed vs not fixed. The fix rate could then be an indicator -of repairability of a particular category (although other factors, such as skill -sets of repairers, also have a factor.) Musical instruments, toys, headphones -and lamps have high fix rates. Flat screens and heating/cooling appliances have -low fix rates (however they are not that frequently brought to events.) -We also have trouble with PC accessories like mice, keyboards, and computer -speakers, kettles, decorative and/or safety lights. - -Spare parts -: 18.8% of repairs require spare parts. In exceptional cases, especially for -screen repairs, participants bring the parts with them and we are able to fix -the device on the spot. Most require follow-up by the device owner. -Spare parts for some devices and categories of devices are found for sale -online. - - - - -NOTE - start making notes on which terminology is likely to be different across clients of the software. \ No newline at end of file diff --git a/features/01-Overview/5-Personas.feature b/features/01-Overview/5-Personas.feature deleted file mode 100644 index 3abdd1857a..0000000000 --- a/features/01-Overview/5-Personas.feature +++ /dev/null @@ -1,4 +0,0 @@ -Feature: Personas - -* Professor Hubert J. Farnsworth is an Admin. -* Fry is a Restart Host with the group Planet Express. \ No newline at end of file diff --git a/features/02-Login_Register_Onboarding/01-Features_Onboarding.feature b/features/02-Login_Register_Onboarding/01-Features_Onboarding.feature deleted file mode 100644 index ceeb457760..0000000000 --- a/features/02-Login_Register_Onboarding/01-Features_Onboarding.feature +++ /dev/null @@ -1,28 +0,0 @@ -Feature: Features/Onboarding - As a prospective user - In order to find out about why I should join the Restarters community - I want to see an easy-to-understand overview of the benefits of joining - -Scenario: Unregistered user views onboarding information - Given the user is unregistered - When the user visits the features page - Then the user should be presented with the onboarding text and images - -# Registered users should be able to view the onboarding information if they want to. -Scenario: Registered user views onboarding information - Given the user is registered - When the user visits the features page - Then the user should be presented with the onboarding text and images - -Scenario: Unregistered user starts sign up process - Given the user is unregistered - When the user visits the features page - And clicks the sign up button - Then they will land on select skills page - -Scenario: Registered user starts sign up process - Given the user is registered - When the user visits the features page - And clicks the sign up button - Then they will be shown a message saying 'You are already registered!' - And they will be taken to the dashboard \ No newline at end of file diff --git a/features/02-Login_Register_Onboarding/02-Register/01-SelectSkills.feature b/features/02-Login_Register_Onboarding/02-Register/01-SelectSkills.feature deleted file mode 100644 index 073a6331ee..0000000000 --- a/features/02-Login_Register_Onboarding/02-Register/01-SelectSkills.feature +++ /dev/null @@ -1,21 +0,0 @@ -Feature: Selecting skills - As a community organiser - In order to help group hosts organise their events - I want volunteers to list their skills when they join - - As a volunteer - In order to help hosts assign me to tasks during events - I want to list my skills when I register - -Scenario: User selects some skills and clicks next - Given the user is registering and is on the select skills step - When the user selects at least one option from the list of skills - And click on Next step button - Then the user lands on About and Register page - -# Although useful, selecting skills is optional. -Scenario: User selects no skills and clicks next - Given the user is registering and is on the select skills step - When the user does not select any option from the list of skills - And click on Next Step button - Then the user lands on About and Register page \ No newline at end of file diff --git a/features/02-Login_Register_Onboarding/02-Register/02-AboutRegister.feature b/features/02-Login_Register_Onboarding/02-Register/02-AboutRegister.feature deleted file mode 100644 index d598745917..0000000000 --- a/features/02-Login_Register_Onboarding/02-Register/02-AboutRegister.feature +++ /dev/null @@ -1,31 +0,0 @@ -Feature: About and Register - As a user - In order to complete the sign up process - I want to be able to fill the fields in tell us about yourself section - - Background: - Given the user accounts have not been created yet - -Scenario: Filling the details correctly -# The fields marked with asterick are mandatory to fill - When a user enters all the data needed as follows - | Your name | Age | Gender | Email Address | Country | Town/City (optional) | Password | Repeat password | - | hubert | hubert! | Admin | hubert@planetexpress.com | Australia | | dfgdf | dfgdf | - | fry | fry! | Host | fry@planetexpress.com | UK | London | !fghg | !fghg | - And clicks on next step button - Then the user is taken to Email alert preference page - -Scenario: Password Validation -# Password validation rules - When a user types the password in confirm password field, it should match with password entered before in the password field - And the password should be equal to or more than six characters - Then the user will be set up with new password and continue to next process. - -Scenario: User wants to go to previous step - When a user wants to go to previous step, click Previous step link - Then the user lands on previous page i.e., select skills page - -Scenario: User can only signup to the application if age>=18 - When a user wants to signup for the application, in the age field there is a restriction of age>=18 - And the user can select the year from the dropdown - Then the user can enter the year if greater than or equal to 18. \ No newline at end of file diff --git a/features/02-Login_Register_Onboarding/02-Register/03-EmailPreferences.feature b/features/02-Login_Register_Onboarding/02-Register/03-EmailPreferences.feature deleted file mode 100644 index cd9ae0e272..0000000000 --- a/features/02-Login_Register_Onboarding/02-Register/03-EmailPreferences.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: Email Preferences - As a User - In order to get notified by the Restart Project - I should signup for email alerts and save the preferences - - Background: - Given the user accounts have not been created yet - -Scenario: Check Email preferences - When a user wants to get notified by the Restart Project - And ticking-off the checkbox and click on next step button - Then she should land on Data consent page. - -Scenario: User wants to go to previous step - When a user wants to go to previous step, click Previous step link - Then the user lands on previous page i.e., select skills page \ No newline at end of file diff --git a/features/02-Login_Register_Onboarding/02-Register/04-DataConsent.feature b/features/02-Login_Register_Onboarding/02-Register/04-DataConsent.feature deleted file mode 100644 index 05478d5973..0000000000 --- a/features/02-Login_Register_Onboarding/02-Register/04-DataConsent.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: Data Consent - As a User - In order to know how my data is to be used - I should give my acceptance to Restartproject - - Background: - Given the user accounts have not been created yet - -Scenario: Check preferences - When a user gives acceptance to his/her data to be used by the Restartproject - And ticking-off the checkbox and click on Complete my profile button - Then user should land on dashboard page with pop up of onboarding process. - -Scenario: User wants to go to previous step - When a user wants to go to previous step, click Previous step link - Then the user lands on previous page i.e., select skills page \ No newline at end of file diff --git a/features/02-Login_Register_Onboarding/02-Register/05-CompleteRegistration.feature b/features/02-Login_Register_Onboarding/02-Register/05-CompleteRegistration.feature deleted file mode 100644 index 469d9d687e..0000000000 --- a/features/02-Login_Register_Onboarding/02-Register/05-CompleteRegistration.feature +++ /dev/null @@ -1,18 +0,0 @@ -Feature: Complete Registration - As a User - In order to use the community platform and view all the events and other platforms - I should register myself onto the community platform and the system should create an account when I register. - -Background: - Given the following account have been created as a user - | Email | Password | - | jenny@google.co.uk | dean1 | - -Scenario: System creating an account when I register - When a user gets registere themselves on the community platform - Then an account should be created within the system. - -Scenario: Creating accounts on Wiki and Discourse. - When a user creats an account onto the system - Then the user would automatically creates an account on Wiki and Discourse with same details - And directly login in wiki and discourse. \ No newline at end of file diff --git a/features/02-Login_Register_Onboarding/02-Register/Registering-a-new-account.feature b/features/02-Login_Register_Onboarding/02-Register/Registering-a-new-account.feature deleted file mode 100644 index 7f27a17638..0000000000 --- a/features/02-Login_Register_Onboarding/02-Register/Registering-a-new-account.feature +++ /dev/null @@ -1,14 +0,0 @@ -Feature: Registering a new account - -Scenario: valid registration - Given I am on the registration page - # Navigate to https://restarters.dev/user/register - When I complete all of the registration details - # Fill in values for step 1: skills - # Fill in values for step 2: profile and account info - # Fill in values for step 3: newsletter opt-in - # Fill in values for step 4: data consent - And I complete my registration - # Click the register button - Then an account is created for me - # Check you have a new account and are on the dashboard diff --git a/features/02-Login_Register_Onboarding/03-SignIn.feature b/features/02-Login_Register_Onboarding/03-SignIn.feature deleted file mode 100644 index 1188f50650..0000000000 --- a/features/02-Login_Register_Onboarding/03-SignIn.feature +++ /dev/null @@ -1,24 +0,0 @@ -Feature: User Authentication - As a user - In order to perform what I want to do on the site - I want to be able to log in - -Background: - Given the following user accounts have been created - | Email | Password | - | fry@planetexpress.com | fry! | - -Scenario: Valid login - When a user logs in with email "fry@planetexpress.com" and password "fry!" - Then the user is logged in as "Fry" with email "fry@planetexpress.com" - -Scenario: Valid login with alternate case email -# Emails are case-insensitive. - When a user logs in with email "FRY@PlAnetExPreSs.com" and password "fry!" - Then the user is logged in as "Fry" with email "fry@planetexpress.com" - -Scenario: Invalid login due to password casing -# Passwords are case-sensitive. - When a user logs in with email "fry@planetexpress.com" and password "FRY!" - Then the user is not logged in - And a message is displayed to the user letting them know they have not been logged in diff --git a/features/02-Login_Register_Onboarding/04-ForgotPassword.feature b/features/02-Login_Register_Onboarding/04-ForgotPassword.feature deleted file mode 100644 index c573eaf4af..0000000000 --- a/features/02-Login_Register_Onboarding/04-ForgotPassword.feature +++ /dev/null @@ -1,29 +0,0 @@ -Feature: Forgot Password - As a User - In order to get a new password - I should be able to do that in forgot password page. - - Given the following user accounts have been created - | Email | Role | - | hubert@planetexpress.com | User | - -Scenario: Forgot Password - When a user completes the fields as follows - | Email address | - | hubert@planetexpress.com | - And clicks on reset button - Then user should land on same page with a message saying the please check your email and follow. - -Scenario: Invalid email ID - When a user enters wrong email id or the email id is not present in database - And clicks reset button - Then the user lands on same page with an error. - -Scenario: I remembered Password - When a user remembers the password - And clicks on the link I remembered. Let me sign in - Then the user lands on login page. - -Scenario: User triggers password reset request email - When the user clicks the forgot password link - Then the user would receive an email to his registered email account, to reset password. diff --git a/features/02-Login_Register_Onboarding/05-ResetPassword.feature b/features/02-Login_Register_Onboarding/05-ResetPassword.feature deleted file mode 100644 index bc1c71d5f1..0000000000 --- a/features/02-Login_Register_Onboarding/05-ResetPassword.feature +++ /dev/null @@ -1,21 +0,0 @@ -Feature: Reset Password - As a User - In order to reset my password - I should be able to do that in password reset page. - - Given the following user accounts have been created - | Email | Role | - | hubert@planetexpress.com | User | - -Scenario: Reset Password - When a user fills the data as follows - | Password | Repeat password | - | hubert! | hubert! | - And clicks on change password button - Then user should land on login page with a message saying the password has been successfully changed. - -Scenario: Password Validation -# Password validation rules - When a user types the password in confirm password field, it should match with password entered before in the password field - And the password should be equal to or more than six characters - Then the user will be set up with new password and continue to next process. \ No newline at end of file diff --git a/features/02-Login_Register_Onboarding/SSO/Login-to-Discourse.feature b/features/02-Login_Register_Onboarding/SSO/Login-to-Discourse.feature deleted file mode 100644 index 1fc123c57f..0000000000 --- a/features/02-Login_Register_Onboarding/SSO/Login-to-Discourse.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: Automatic login to Discourse - -Scenario: logging in to Restarters logs in to Discourse - Given I login with a valid user - # Navigate to https://restarters.dev - # Enter valid login details - When I navigate to Talk - # Navigate to https://talk.restarters.dev via the global nav - Then I can see that I am already logged in to Talk - # Check for presence of user menus to indicate logged in diff --git a/features/02-Login_Register_Onboarding/SSO/Login-to-Wiki.feature b/features/02-Login_Register_Onboarding/SSO/Login-to-Wiki.feature deleted file mode 100644 index e370f60a5a..0000000000 --- a/features/02-Login_Register_Onboarding/SSO/Login-to-Wiki.feature +++ /dev/null @@ -1,14 +0,0 @@ -Feature: Automatic login to the Wiki - -When a user joins Restarters they are created an account on the Wiki. -The Wiki runs on MediaWiki. -When they log in to Restarters, they are automatically logged in to MediaWiki. - -Scenario: Logging in logs in to Wiki - Given I log in with a valid user - # Navigate to https://restarters.dev - # Enter login details - When I navigate to the Wiki - # Navigate to https://wiki.restarters.dev via the global nav - Then I can see that I am already logged in to the Wiki - # Check for presence of user menus diff --git a/features/03-Dashboard/Dashboard.feature b/features/03-Dashboard/Dashboard.feature deleted file mode 100644 index f97169c39d..0000000000 --- a/features/03-Dashboard/Dashboard.feature +++ /dev/null @@ -1,62 +0,0 @@ -Feature: Dashboard - -# Welcome text - -Scenario: Intro text - Given that I am any logged in user - When I visit the dashboard - Then I see the 'Getting started' info column on the right - -# Talk - -Scenario: Latest Talk - Given that I am any logged in user - When I visit the dashboard - Then I see the latest Discourse topics - -# Groups section - -Scenario: User has followed a group with an upcoming event - Given I am a user - And I have followed at least 1 group that has at least 1 upcoming event - Then I see a list of the group(s) I follow (orderered alphabetical in the MVP) and the upcoming events (ordered by soonest first) for that/those group(s) in the my groups - -Scenario: Host of a group with no upcoming events - Given I am a host of a group that has no upcoming events - Then I see a list of my groups and a message encouraging me to add events for my group(s) - - -Scenario: User that hasn't followed any groups - Given I am a user and I haven’t followed any groups yet - Then I see a message inviting me to find groups in my area - -# Add data section - -Scenario: User has RSVPed to at least 1 event - Given I am any user and I have RSVPed to at least 1 event that has started (or finished) - When I visit the dashboard - Then I see the Add Data section - -Scenario: User not RSVPed to any events - Given I am a user that has not RSVPed to any events - When I visit the dashboard - Then I do not see the Add Data section - -Scenario: Add Data section - Given I can see the ‘Add Data’ section - Then the most recent event I have RSVPd to (and organising group) appear pre-selected in the drop down menus - And the drop down menu for group is sorted alphabetically - And the drop down menu for events is sorted reverse chronologically (most recent event at the top) - -# New groups - -Scenario: New group nearby - Given a group has been created in the last month - And it is within the currently logged in user's area - And the user is not currently a member of that group - Then the count of new groups in your area is incremented by 1 - -Scenario: Clicking through to new groups - Given I click on the ‘Newly added: X groups in your area!’ - Then this takes me through to the /group page with the ‘Groups nearest to you' - And new groups are flagged with a ‘New’ label diff --git a/features/03-Dashboard/DashboardFirstVisit_host.feature b/features/03-Dashboard/DashboardFirstVisit_host.feature deleted file mode 100644 index ce772d5a7c..0000000000 --- a/features/03-Dashboard/DashboardFirstVisit_host.feature +++ /dev/null @@ -1,42 +0,0 @@ -Feature: View of Dashboard for the first time when a host sign up on the community platform - As a Host - In order to view the dashboard - I should be able to signup as a host on the community platform. - -Background: - Given the following account have been created as a host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View Dashboard -# View dashboard consisting all the activities to bo done on the platform - When a host lands on dashboard - Then he would view all the activities that he can do with a journey of updating your profile. - -Scenario: About Getting started - When host lands on dashboard, the getting started column is useful to build your profile - Then the host can build his profile by clicking the links and following the process. - -Scenario: Activities present on dashboard - When host lands on dashboard, he can view Getting started in community repair, How to host an event, Discussion, Wiki and Community news - Then the host should explore by clicking the links provided all the categories to get familiar with the platforms. - -Scenario: Host clicks on view the materials link on Getting started in community repair blog on dashboard - When host clicks on view the materials link on dashboard - Then he will be landed on About the repair in your community category post on Discourse. - -Scenario: Host clicks on view the materials link in How to host an event blog on dashboard - When host clicks on view the materials link on dashboard - Then he will be landed on how to run a repair event post on Discourse. - -Scenario: Host clicks on Join the discussion link on Discussion blog on dashboard - When host clicks on Join the discussion link on dashboard - Then he will be landed on the homepage of the Discourse. - -Scenario: Host clicks on any links in Wiki blog on dashboard - When host clicks on the links in wiki blog on dashboard - Then he will be landed on wiki page of that particular link. - -Scenario: Host clicks on any links in the community news on dashboard - When host clicks on the links in wiki blog on dashboard - Then he will be landed on The Restart Project pages depending on the link. \ No newline at end of file diff --git a/features/03-Dashboard/DashboardFirstVisit_restarter.feature b/features/03-Dashboard/DashboardFirstVisit_restarter.feature deleted file mode 100644 index 5074115efc..0000000000 --- a/features/03-Dashboard/DashboardFirstVisit_restarter.feature +++ /dev/null @@ -1,42 +0,0 @@ -Feature: View of Dashboard for the first time when a restarter sign up on the community platform - As a Restarter - In order to view the dashboard - I should be able to signup as a restarter on the community platform. - -Background: - Given the following account have been created as a restarter - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View Dashboard -# View dashboard consisting all the activities to bo done on the platform - When a restarter lands on dashboard - Then he would view all the activities that he can do with a journey of updating your profile. - -Scenario: About Getting started - When restarter lands on dashboard, the getting started column is useful to build your profile - Then the restarter can build his profile by clicking the links and following the process. - -Scenario: Activities present on dashboard - When restarter lands on dashboard, he can view Discussion, Getting started in community repair, Upcoming events, Wiki and Community news - Then the restarter should explore(by clicking the links provided) all the categories to get familiar with the platform. - -Scenario: Restarter clicks on Join the discussion link on Discussion blog on dashboard - When restarter clicks on Join the discussion link on dashboard - Then he will be landed on the homepage of the Discourse. - -Scenario: Restarter clicks on view the materials link on Getting started in community repair blog on dashboard - When restarter clicks on view the materials link on dashboard - Then he will be landed on community values post on Discourse. - -Scenario: Restarter clicks on see all events link on Upcoming events blog on dashboard - When restarter clicks on see all events link on dashboard - Then he will be landed on view all events page. - -Scenario: Restarter clicks on any links in Wiki blog on dashboard - When restarter clicks on the links in wiki blog on dashboard - Then he will be landed on wiki page of that particular link. - -Scenario: Restarter clicks on any links in the community news on dashboard - When restarter clicks on the links in wiki blog on dashboard - Then he will be landed on The Restart Project pages depending on the link. \ No newline at end of file diff --git a/features/03-Dashboard/Dashboard_host.feature b/features/03-Dashboard/Dashboard_host.feature deleted file mode 100644 index 69d3f97fa3..0000000000 --- a/features/03-Dashboard/Dashboard_host.feature +++ /dev/null @@ -1,44 +0,0 @@ -Feature: View of Dashboard after log in on the community platform - As a Host - In order to view the dashboard - I should be able to login as a host on the community platform. - -Background: - Given the following account have been created as a host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View Dashboard - When a host lands on dashboard - Then he would view all the activities that he can do and that is going on and that has been done on the platform. - -Scenario: Activities present on dashboard - When host lands on dashboard, he can view Creat new event, Your recent events, How to host an event, Restarters in your area, Discussion, wiki and Community news - Then the host should be able to navigate(by clicking the links provided) through categories according to their use. - -Scenario: Host clicks on create new event link on create new event blog on dashboard - When host clicks on create new event link on dashboard - Then he will be landed on create new event page. - -Scenario: Host clicks on Your recent events links on Your recent events blog on dashboard - When host clicks on see all events link or on a particular event link on dashboard - Then he will be landed on all events page or on that particular event page respectively. - -Scenario: Host clicks on view the materials link on how to host an event blog on dashboard - When host clicks on view the materials link on dashboard - Then he will be landed on How to run a repair event post on Discourse. - -Scenario: Host clicks on Restarters in your area blog on dashboard -#to be developed - -Scenario: Host clicks on Join the discussion link on Discussion blog on dashboard - When host clicks on Join the discussion link on dashboard - Then he will be landed on the homepage of the Discourse. - -Scenario: Host clicks on any links in Wiki blog on dashboard - When host clicks on the links in wiki blog on dashboard - Then he will be landed on wiki page of that particular link. - -Scenario: Host clicks on any links in the community news on dashboard - When host clicks on the links in wiki blog on dashboard - Then he will be landed on The Restart Project pages depending on the link. \ No newline at end of file diff --git a/features/03-Dashboard/Dashboard_restarter.feature b/features/03-Dashboard/Dashboard_restarter.feature deleted file mode 100644 index 2b3d013baa..0000000000 --- a/features/03-Dashboard/Dashboard_restarter.feature +++ /dev/null @@ -1,41 +0,0 @@ -Feature: View of Dashboard after log in on the community platform - As a Restarter - In order to view the dashboard - I should be able to login as a restarter on the community platform. - -Background: - Given the following account have been created as a restarter - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View Dashboard - When a restarter lands on dashboard - Then he would view all the activities that he can do and that is going on and that has been done on the platform. - -Scenario: Activities present on dashboard - When restarter lands on dashboard, he can view Discussion, Upcoming events, Getting started in community repair, Your recent events, Wiki and Community news - Then the restarter should be able to navigate(by clicking the links provided) through categories according to their use. - -Scenario: Restarter clicks on Join the discussion link on Discussion blog on dashboard - When restarter clicks on Join the discussion link on dashboard - Then he will be landed on the homepage of the Discourse. - -Scenario: Restarter clicks on see all events link on Upcoming events blog on dashboard - When restarter clicks on see all events link on dashboard - Then he will be landed on view all events page. - -Scenario: Restarter clicks on view the materials link on Getting started in community repair blog on dashboard - When restarter clicks on view the materials link on dashboard - Then he will be landed on community values post on Discourse. - -Scenario: Restarter clicks on Your recent events links on Your recent events blog on dashboard - When restarter clicks on see all events link or on a particular event link on dashboard - Then he will be landed on all events page or on that particular event page respectively. - -Scenario: Restarter clicks on any links in Wiki blog on dashboard - When restarter clicks on the links in wiki blog on dashboard - Then he will be landed on wiki page of that particular link. - -Scenario: Restarter clicks on any links in the community news on dashboard - When restarter clicks on the links in wiki blog on dashboard - Then he will be landed on The Restart Project pages depending on the link. \ No newline at end of file diff --git a/features/03-Dashboard/Onboarding.feature b/features/03-Dashboard/Onboarding.feature deleted file mode 100644 index c5a58428e4..0000000000 --- a/features/03-Dashboard/Onboarding.feature +++ /dev/null @@ -1,24 +0,0 @@ -Feature: Onboarding steps - As a User - In order to know how the platform works - I should see onboarding process - - Background: - Given the following account have been created as a user - | Email | Password | - | jenny@google.co.uk | dean1 | - -Scenario: Onboarding process - When a user sees the onboarding process - And click on next button or previous button - Then user sees next or previous part of onboarding process. - -Scenario: Clicking on Create new party - When a user wants to create a party after going through the onboarding process - And clicks on create new party button - Then the user lands on party creation page. - -Scenario: Clicking on cancel - When a user wants to go to dashboard after going through the onbosrding process - And clicks cancel symbol X - Then the user lands on dashboard page. \ No newline at end of file diff --git a/features/03-Dashboard/Volunteers_engagement.feature b/features/03-Dashboard/Volunteers_engagement.feature deleted file mode 100644 index 118151f90e..0000000000 --- a/features/03-Dashboard/Volunteers_engagement.feature +++ /dev/null @@ -1,19 +0,0 @@ -Feature: Volunteers engagement on Talk -#Volunteer engagement. Talk is a very important of the platform, where people can get involved and be active even if there are no events or groups currently near them. -#We want to highlight activity and encourage participation and use of Talk as much as possible. - As a user - In order to communicate with other volunteers - I should be able to navigate to discourse. - -Background: - Given the following account have been created - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Navigating to discourse - When a user clicks on the hot topic list link on dashboard - Then he will be taken to talk, hot topics list. - -Scenario: User permissions on discourse - When a user clicks through the discourse link - Then user will be seen only topics that are in categories they have right to view. diff --git a/features/04-Events/Create/AddAnEvent.feature b/features/04-Events/Create/AddAnEvent.feature deleted file mode 100644 index 68407f6d0d..0000000000 --- a/features/04-Events/Create/AddAnEvent.feature +++ /dev/null @@ -1,62 +0,0 @@ -Feature: Add an event - As a User (Host, Admin) - In order to add a new event - I should be able to do by navigating to add an event page - -Background: - Given the following account have been created as a host or an admin - | Email | Password | Role | - | dean@wecreatedigital.co.uk | dean | Host | - | hello@howareyou.com | hello | Admin | - -Scenario: Create a new event -# View all events i.e., past events, upcoming events and moderate events - When a host clicks on event page and fills the data as follows - | Name of event | Event group | Description | Date of event | Start/end time | Venue address | Add event image | - | Ram | vanarulu | exp group in fixing things | 7/6/2018 | 20-24 | Remakery | Add event image | - Then he lands on events page and can see all the events in that page. - -Scenario: Saving a new event - When a host enters all the data needed to create an event - And clicks on save button - Then a success message should appear on the same page. - -Scenario: Text cleaned in the description - When a host copies and paste into the description box - And the data should loose all htmls and css properties it has - Then it show a message inside description box as text cleaned. - -Scenario: Calender pop-up on Date of event - When a host clicks on date field, calendar should pop up - And select a date when to arrange party - Then host lands on the same page and continues with next process. - -Scenario: When clicked on start time, automatically generate 3hr+ as end time - When a host clicks on start time, automatically from then +3hr time is calculated as follows - | Start/end time | - | 13:00 16:00 | - Then that time is stored in the end time field. - -Scenario: How to find Venue adddress - When a host clicks on venue address, types the address - Then automatically suggestions should show up and the place should be pointed in map. - -Scenario: searching the image -#TODO: when clicked on add group image here text, file explorer opens. - When user clicks on add image text, then file explorer should open - And browse for the image - And select the one needed - Then you will see the uploaded image thumbnail in that area. - -Scenario: User triggers notification email about event creation to admin - When the user clicks on save event button - Then the admin would receive an notification email about event creation for moderation. - -Scenario: Allow for upload of multiple event images - When user selects multiple images and click on upload button - Then all the images should be uploaded with view of their thumbnails. - -Scenario: Restarter cannot create an event - Given logged in as a restarter - When the user is on the list of events page - Then there should be no create event button. \ No newline at end of file diff --git a/features/04-Events/Delete/DeleteEvent.feature b/features/04-Events/Delete/DeleteEvent.feature deleted file mode 100644 index c4cb16d707..0000000000 --- a/features/04-Events/Delete/DeleteEvent.feature +++ /dev/null @@ -1,98 +0,0 @@ -Feature: Delete Event - -In order to keep the list of events tidy, -As a host or an admin -I want to be able to delete events that should not be in the application - -The simplest use case is deleting events that have not been moderated, and -do not have any volunteers attached to them. Either as RSVP or invitation. - -We generally do not want to delete events that volunteers have been invited -to or are attending. - -We do not want to delete events that have device data associated with them. -It's possible there will be some rare cases that we need to do that as an -Administrator, but it should only be an administrator level action. As it -should only be in very rare cases, for now it is left as such that the only -way to do it is to delete all devices, remove all people associated with the -event, and then delete. If it ever became a regularly required thing, we could -add a single button to do that for Admins, but this is unlikely. - -To keep it simple for now, we could just only allow deletion when no volunteers -are associated. And you need to remove yourself from the event? It makes it a -lot easier to implement, however, it doesn't make much sense from a user perspective. - -# Fail -# Doesn't display a notification message on return to list of events -Scenario: Unmoderated event deleted successfully - When I successfully delete an event - Then I am returned to the list of events - And the event is no longer displayed in the application - And I see a message saying 'Event successfully deleted' - -# Fail -# From the code, doesn't look like it will remove the event from therestartproject.org -# Could this be confirmed? -Scenario: Moderated event deleted successfully - When I successfully delete an event - Then the event is removed from the list of events - And I am returned to the list of events - And I see a message saying 'Event successfully deleted' - And the event is removed from the list of events on therestartproject.org - -# Pass -Scenario: Admin tries to delete event they did not create, with no volunteers associated - Given I am an administrator - And I am viewing an event that I did not create - And the event has with no volunteers associated - When I press the delete event button - Then I am allowed to delete the event - -# Pass -Scenario: Admin tries to delete event they did not create, with some volunteers associated - Given I am an administrator - And I am viewing an event that I did not create - And the event has some volunteers associated - When I press the delete event button - Then I am not allowed to delete the event - And I am shown a message saying 'Sorry, you cannot delete this event while there are volunteers associated.' - -# Fail -# They are shown the message 'Sorry you cannot delete this event as you have invited other volunteers' -Scenario: Admin tries to delete event they created, with no other volunteers associated - Given I am an administrator - And I am viewing an event that I created - And the event has only myself associated - When I press the delete event button - Then I am allowed to delete the event - -# Pass -Scenario: Admin tries to delete event they created, with other volunteers associated - Given I am an administrator - And I am viewing an event that I created - And the event has other volunteers associated - When I press the delete event button - Then I am not allowed to delete the event - And I am shown a message saying 'Sorry, you cannot delete this event while there are volunteers associated.' - -# Fail -# Two issues -# Have to remove the myself first - otherwise shown message saying I have invited others -# After doing that, I am correctly shown message saying 'Are you sure you want to delete this event?' -# However when I click OK, I get the message 'You do not have permission to delete this event' -Scenario: Host tries to delete event they created, with no other volunteers associated - Given I am a host - And I am viewing an event that I created - And the event has with no volunteers associated (I am the only volunteer associated) - When I press the delete event button - Then I am allowed to delete the event - -# Pass -# Theoretically working, as it's blocking if any volunteers attached -Scenario: Host tries to delete event they created, with other some volunteers associated - Given I am a host - And I am viewing an event that I created - And the event has some other volunteers associated - When I press the delete event button - Then I am not allowed to delete the event - And I am shown a message saying 'Sorry, you cannot delete this event while there are volunteers associated.' \ No newline at end of file diff --git a/features/04-Events/Delete/DeleteFromWordPress.feature b/features/04-Events/Delete/DeleteFromWordPress.feature deleted file mode 100644 index 2dde79c773..0000000000 --- a/features/04-Events/Delete/DeleteFromWordPress.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: Delete event from WordPress - -As a group host, -I want event that I delete to also be removed from therestartproject.org, -So that they are no longer publicly visible. - -If it isn’t deleted from WordPress successfully, admins should be notified. - -Scenario: Event deleted OK - When a user deletes an event from Restarters.net - Then it should also be removed from therestartproject.org - -Scenario: Event not deleted successfully - When a user deletes an event from Restarters.net - And it is not deleted from WordPress successfully at the time - Then admins should be sent a notification to let them know diff --git a/features/04-Events/Editing/AmendNumberOfVolunteers.feature b/features/04-Events/Editing/AmendNumberOfVolunteers.feature deleted file mode 100644 index 94b93ba993..0000000000 --- a/features/04-Events/Editing/AmendNumberOfVolunteers.feature +++ /dev/null @@ -1,45 +0,0 @@ -Feature: Amend number of volunteers - -In order to keep records of estimated number of volunteers -As an Admin or a group host -I need to be able to amend the number of volunteers that attended an event. - -We need a way to manually amend the number of volunteers that attended an event. -Ideally the number would automatically set to the right amount by invites/RSVPs, -but this feature is not being widely enough used yet. - -It's only possible to manually amend the number of volunteers for an event after the event -has started - -Scenario: Can't amend volunteers until event has started -# Pass - -Scenario: Admin amends number of volunteers for event - Given I am an Admin - When I change the number of volunteers for an event - Then the amended value is saved -# Pass - -Scenario: Host of a group amends number of volunteers for event - Given I am a host of a group - When I change the number of volunteers for an event from my group - Then the amended value is saved -# Pass - -Scenario: Host of another group should not be able to amend the number of volunteers for event from other group - Given I am a host - When I visit the event page of an event from another group - Then I do not have the option to amend the number of volunteers -# Pass - -Scenario: Manually amended number of volunteers differs from number of volunteers linked - Given I am an Admin or a Host - When I change the number of volunteers - And the new figure is different from the number of volunteers associated with the event - Then I should see a message saying 'Please note that the number of volunteers does not match the attendance record. Do you need to add or remove volunteers?' -# Fail -# Message is incorrect - - -# General Fail -# Somewhat unrelated, but I get the message 'Something went wrong' when editing number of participants \ No newline at end of file diff --git a/features/04-Events/Editing/EditEvent.feature b/features/04-Events/Editing/EditEvent.feature deleted file mode 100644 index afbe7aaaa0..0000000000 --- a/features/04-Events/Editing/EditEvent.feature +++ /dev/null @@ -1,48 +0,0 @@ -Feature: Edit an event - As a User (Host, Admin) - In order to edit a new event - I should be able to do by navigating to edit event page - -Background: - Given the following account have been created as a host or an admin - | Email | Password | Role | - | dean@wecreatedigital.co.uk | dean | Host | - | hello@howareyou.com | hello | Admin | - -Scenario: Editing a event - When a host clicks on edit event page and changes/updates the data as follows - | Name of event | Event group | Description | Date of event | Start/end time | Venue address | Add event image | - | Ram | vanarulu | group in fixing things | 7/6/2018 | 20-24 | Remakery | Add event image | - And clicks on save party button - Then host lands on all events page with the edited event in the list of events. - -Scenario: Text cleaned in the description - When a host copies and paste into the description box - And the data should loose all htmls and css properties it has - Then it show a message inside description box as text cleaned. - -Scenario: Calender pop-up on Date of event - When a host clicks on date field, calendar should pop up - And select a date when to arrange party - Then host lands on the same page and continues with next process. - -Scenario: When clicked on start time, automatically generate 3hr+ as end time - When a host clicks on start time, automatically from then +3hr time is calculated as follows - | Start/end time | - | 14:00 17:00 | - Then that time is stored in the end time field. - -Scenario: How to find Venue adddress - When a host clicks on venue address, types the address - Then automatically suggestions should show up and the place should be pointed in map. - -Scenario: searching the image -#TODO: when clicked on add group image here text, file explorer opens. - When user clicks on add image text, then file explorer should open - And browse for the image - And select the one needed - Then you will see the uploaded image thumbnail in that area. - -Scenario: Admin triggers view event email - When the admin clicks the approve event button - Then the host would receive an email about confirmation of that event. \ No newline at end of file diff --git a/features/04-Events/Editing/EventPermissions.feature b/features/04-Events/Editing/EventPermissions.feature deleted file mode 100644 index 9607c58c2b..0000000000 --- a/features/04-Events/Editing/EventPermissions.feature +++ /dev/null @@ -1,20 +0,0 @@ -Feature: Event Permissions - -If there are multiple hosts of a group, then if one host creates an event, -then other hosts of the same group should be able to edit the event. - -Background: - Given the following groups: - | Name | - | Hackney Fixers | - And the following hosts: - | Name | Group | - | Fry | Hackney Fixers | - | Leyla | Hackney Fixers | - -Scenario: Permission to edit an event - Given Fry has created the following event: - | Name | - | Big Fix | - When Leyla tries to edit the event 'Big Fix' - Then she is able to do so \ No newline at end of file diff --git a/features/04-Events/Invitations/InvitationNotifications.feature b/features/04-Events/Invitations/InvitationNotifications.feature deleted file mode 100644 index d1e15abe5e..0000000000 --- a/features/04-Events/Invitations/InvitationNotifications.feature +++ /dev/null @@ -1,31 +0,0 @@ -Feature: Invitation notifications - As a volunteer - In order to keep track of the events I am attending - I want to be notified when I have been invited to volunteer at an event - -Volunteers are notified when they have been invited to an event. They will receive an in-app notification, -and, if they have opted-in to email notifications, they will also receive an email notification. - -The email should look as below (right-click and view to see full size): - - - -Background: - Given the following users: -| Name | Role | Receive invites? | -| Leila | Host | Yes | -| Fry | Restarter | No | - -Scenario: Invitation to volunteer already on platform, opted-in to emails - When Leila is invited to an event - Then Leila receives a in-app notification letting them know that they have been invited - And Leila receives an email notification - -Scenario: Invitation to volunteer already on platform, opted-out of emails - When Fry is invited to an event - Then Fry receives an in-app notification letting them know that they have been invited - -Scenario: Invitation to volunteer not already on platform - When a new volunteer, without an account on the platform, is invited to an event - Then the volunteer receives an email inviting them to the event - diff --git a/features/04-Events/Invitations/InviteVolunteers.feature b/features/04-Events/Invitations/InviteVolunteers.feature deleted file mode 100644 index ee4e6a8d21..0000000000 --- a/features/04-Events/Invitations/InviteVolunteers.feature +++ /dev/null @@ -1,28 +0,0 @@ -Feature: Send event invites to volunteers - As a host - In order to help boost volunteer attendance at events - I should be able to invite volunteers to events - -Hosts can send invitations to volunteers inviting them to come to their event. - -Background: - Given the following accounts: - | Email | Password | - | fry@planetexpress.com | fry! | - -Scenario: Inviting volunteers to an event - When a user clicks on invite button, invite restarters a pop up screen is displayed - And user can check the checkbox so that all the restarters associated in that group will get the invite or host can send invites manually by entering the email address of the restarter as follows - | Email address | - | d@wcd.co.uk | - And also can send an invitation message in the textarea provided as follows - | Invitation message | - | Hi, Hope to see at the event! | - And click on send invite button - Then host will land on event page with number of invites in the attendace section also a message saying the invites have been sent successfully. - -Scenario: Invalid email address - When a user gives invalid email address - And clicks on send invite button - Then an error message will display - diff --git a/features/04-Events/Invitations/images/invitation-email-not-on-platform.jpg b/features/04-Events/Invitations/images/invitation-email-not-on-platform.jpg deleted file mode 100644 index 86d98d0fd2..0000000000 Binary files a/features/04-Events/Invitations/images/invitation-email-not-on-platform.jpg and /dev/null differ diff --git a/features/04-Events/InvitedRestarters.feature b/features/04-Events/InvitedRestarters.feature deleted file mode 100644 index b83885e85a..0000000000 --- a/features/04-Events/InvitedRestarters.feature +++ /dev/null @@ -1,14 +0,0 @@ -Feature: View all invited restarters - As a User (All roles) - In order to view all invited restarters - I should be able to click on see all invited link and view the list of restarters. - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View all the invited restarters - When a user clicks on see all invited link in the events page - Then a pop up appears with all the list of restarters that have been invited - And can view the restarter name with their skills. \ No newline at end of file diff --git a/features/04-Events/Listings/ListUpcoming.feature b/features/04-Events/Listings/ListUpcoming.feature deleted file mode 100644 index cd7c8535a7..0000000000 --- a/features/04-Events/Listings/ListUpcoming.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: List upcoming events - -Scenario: No filter - When I don't filter by anything - Then I can see all upcoming events - -Scenario: Filter by date from - When I filter by a from date - Then I can only see events on or after that date - -Scenario: Filter by date to - When I filter by a to date - Then I can only see events on or before that date - -Scenario: Filter by online - When I filter by online status - Then I can only see events that are online diff --git a/features/04-Events/Listings/ViewMyEvents.feature b/features/04-Events/Listings/ViewMyEvents.feature deleted file mode 100644 index 7c1f0f5045..0000000000 --- a/features/04-Events/Listings/ViewMyEvents.feature +++ /dev/null @@ -1,34 +0,0 @@ -Feature: View my events listings - As any user - In order to keep up to date with events relevant to me - I want to have access to an events listings page - -Scenario: Viewing events page as any user - Given I am any user - When I go to the Events page - Then I see a Your Events section with two tabs: Upcoming and Past - And an Other Events section with two tabs: Nearby and All - -Scenario: Viewing events moderation section as admin or network coordinator - Given that I am an admin or network coordinator - When I go to the Events page - Then I see the Events to Moderate section showing any events to moderate - -Scenario: Add events button - Given that I am a host, admin or network coordinator - When I go to the Events page - Then I see a button to add a new event - -Scenario: RSVPing to events in the listings - Given that I am a member who has not RSVPd to an event - When I go to the Events page, Your Events, Upcoming - Then I should see an RSVP button and two columns, invited and confirmed - -Scenario: RSVPed to an event - Given that I am a member who has RSVPd to an event - When I go to the Events page, Your Events, Upcoming - Then I should see a “You’re going” message and different event styling for the events I have RSVPed to - -Scenario: Events calendar - Given that I am any user - Then I see a button that allows me to subscribe to my events calendar diff --git a/features/04-Events/ManageActivePastEvent_restarter.feature b/features/04-Events/ManageActivePastEvent_restarter.feature deleted file mode 100644 index 2944f279d3..0000000000 --- a/features/04-Events/ManageActivePastEvent_restarter.feature +++ /dev/null @@ -1,40 +0,0 @@ -Feature: Active and past events - As a restarter - In order to see and edit the particular event details - I should be able to navigate to that paarticular event page. - -Background: - Given the following account have been created a restarter - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View active/past events -# View active/past events i.e., past evetns, upcoming events and moderate events - When a restarter clicks on particular event page - And likes to view environmental impact, attendees, event details etc., - Then he can see on that particular event page. - -Scenario: Edit devices section -#Only the restarter who attended the event can only edit a device but cannot add or delete a device from the device section - When a restarter who attended the event wants to edit devices section - And should click on edit option - Then he can view editable options of that device - And save the changes by clicking on save button. - -Scenario: Veiw the attended volunteers - When a restarter wants to view the volunteers who have attended that event - Then he can view in attendace section of the event page. - -Scenario: View the invited volunteers - When a restarter wants to view the number of volunteers invited to the event - Then restarter can see in invited tab. - -Scenario: View device details - When a restarter wants to view the devices that hase been fixed, repairable and end of life - Then can see the list in the devices section - -Scenario: Restarter cannot edit devices if he did not attend event - Given logged in as a restarter who didn't attend the event - When the user is on the edit party devices page - Then there should be no edit button or add button in the devices section - And restarter can view the device only. \ No newline at end of file diff --git a/features/04-Events/ManageActivePastEvents.feature b/features/04-Events/ManageActivePastEvents.feature deleted file mode 100644 index f3cdbee39a..0000000000 --- a/features/04-Events/ManageActivePastEvents.feature +++ /dev/null @@ -1,48 +0,0 @@ -Feature: Manage active/past events - As a User (host or admin) - In order to manage the events - I should be able to navigate to manage an event page. - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Manage active/past events -# Manage active/past events i.e., past evetns, upcoming events and moderate events - When a host clicks on particular event page - And likes to either edit or update any changes or see environmental impact, attendees, event details etc., - Then he can see on that particular event page. - -Scenario: Deleting or adding a volunteer - When a host wants to login the absence of volunteers who RSVPed and presence of volunteers who came directly to the event - And host wants to manage that - Then he can manage it in the attendace section of the event page - And can delete or add a volunteer through the links provided. - -Scenario: Inviting volunteers to the event - When a host wants to invite volunteers to the event, can send invite via emails - And he can do this in the attendance section in invites tab - Then host can see the number of invites sent to the volunteers in that tab. - -Scenario: Add/ Manage a device details - When a host has entered the devices that hase been fixed, repairable and end of life - And can see the list in the devices section - And host wants to either add/update a device then click on add button for a new device - And click on edit link of particular device to be updated and fill the details as follows - | Status | Repair info | Spare parts | Category | Brand | Model | Age | Description of problem/solution | Add image | - | Fixed | More time | Yes | Flat screen TV | Toshiba | 123 | 3 years| Doesn't require memory card | | - Then click on save button - And we can find the new/ updated device in the list of devices. - -Scenario: Automatic Post event device upload reminder email - When 24hours has passed since an event has finished - Then the post event device upload reminder email shouldbe sent to the host of the event. - -Scenario: Host triggers post event device edit reminder email - When the host clicks the send email to restarters button - Then all the restarters that attended the event would receive an email to reminder them to edit device information. - -Scenario: Host/restarter triggers email by marking description of a repair suitable to wiki - When the host/restarter marks the description of a repair suitable to wiki and clicks save - Then admin would receive an email to view the repair notes. \ No newline at end of file diff --git a/features/04-Events/OnlineEvents/CreatingOnlineEvents.feature b/features/04-Events/OnlineEvents/CreatingOnlineEvents.feature deleted file mode 100644 index 78bc78f406..0000000000 --- a/features/04-Events/OnlineEvents/CreatingOnlineEvents.feature +++ /dev/null @@ -1,18 +0,0 @@ -Feature: Creating online events - -In order to continue supporting repair during coronavirus, -As a host, -I would like to create online events - -Online events are not required to have a physical location associated with them. -They will usually have an additional online link associated with them, such as to a place to sign up for a webinar, or a link to an online conference tool. - -Scenario: Marking event as online - Given I have set the event as being online - When I save the event - Then the event is saved as an online event - -Scenario: Leaving location blank - Given I have left the location field empty - When I save the event - Then I do not encounter any errors diff --git a/features/04-Events/OnlineEvents/FilteringOnlineEvents.feature b/features/04-Events/OnlineEvents/FilteringOnlineEvents.feature deleted file mode 100644 index 359d76619d..0000000000 --- a/features/04-Events/OnlineEvents/FilteringOnlineEvents.feature +++ /dev/null @@ -1,4 +0,0 @@ -Feature: Filtering by online events - -Scenario: - Gvien diff --git a/features/04-Events/OnlineEvents/ViewingOnlineEvents.feature b/features/04-Events/OnlineEvents/ViewingOnlineEvents.feature deleted file mode 100644 index 4e6b55fb42..0000000000 --- a/features/04-Events/OnlineEvents/ViewingOnlineEvents.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: Viewing online events - -Scenario: Viewing single online event - When I view an online event - Then I do not see the event map - And I do not see the event location - -Scenario: Viewing online event listings - When I view a list of online events - Then I do not see a value in the event location diff --git a/features/04-Events/RecordVolunteer.feature b/features/04-Events/RecordVolunteer.feature deleted file mode 100644 index 4bda81611e..0000000000 --- a/features/04-Events/RecordVolunteer.feature +++ /dev/null @@ -1,28 +0,0 @@ -Feature: Record Volunteer - As a User (host, admin) - In order to record the volunteer who directly came to the event - I should be able to click on add volunteer button on events pages. - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Recording volunteer's who came to the event directly(no RSVP) - When a user clicks on add volunteer button, a pop up screen of add volunteer is displayed - And fill in the fields as follows - | Group member |Full name | Volunteers email address(optional) | - | Hackney fixers | Dean | d@wcd.co.uk | - | Remakery | John | j@dcw.co.uk | - And click on volunteer attended button - Then host will land on event page with the added volunteer in the list of volunteers attended with a message saying the volunteer has bee successfully recorded. - -Scenario: Invalid email address - When a user gives invalid email id - And clicks on send invite button - Then an error message will display. - -Scenario: Invalid Group name - When a user gives invalid group name - And clicks on send invite button - Then an error message will display. \ No newline at end of file diff --git a/features/04-Events/RestartersAttended.feature b/features/04-Events/RestartersAttended.feature deleted file mode 100644 index d7a25e4198..0000000000 --- a/features/04-Events/RestartersAttended.feature +++ /dev/null @@ -1,15 +0,0 @@ -Feature: View all the attended restarters - As a User (All roles) - In order to view all the attended restarters - I should be able to click on see all invited link and view the list of attended restarters. - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View all the attended restarters - When a user clicks on see all attended link in the events page - Then a pop up appears with all the list of restarters that have attended - And can view the host of that party - And can view the restarter name with their skills and also a link to remove the volunteer. \ No newline at end of file diff --git a/features/04-Events/Stats/EventsFilter.feature b/features/04-Events/Stats/EventsFilter.feature deleted file mode 100644 index 1f4e724d44..0000000000 --- a/features/04-Events/Stats/EventsFilter.feature +++ /dev/null @@ -1,18 +0,0 @@ -Feature: Filter Events - As a User (admin, host) - In order to filter events - I should be able to do so by navigating to events filter page - -Background: - Given the following account have been created as an admin - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: To search for an event - When an admin wants to search for an event, fill the fields as follows - | By group | By event | From date | To date | Group tag | - | Mighty Restarter | history event | 23/04/2017 | 09/11/2018 | Exampletag1 | - | Mighty Restarter | | 23/04/2017 | 09/11/2018 | | - And its not mandatory to fill all the details, they are optional - And click on filter results button - Then he can view the filtered event results year wise in descending order along with other information. \ No newline at end of file diff --git a/features/04-Events/Stats/ShareStats.feature b/features/04-Events/Stats/ShareStats.feature deleted file mode 100644 index c4d0f60353..0000000000 --- a/features/04-Events/Stats/ShareStats.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: Share your Stats - As a User (all roles) - In order to share the stats of a particular event to other place - I should be able to click on Events stats embed button on events pages. - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Sharing your stats - When a user wants to share their stats to other places, click on Events stats embed button - And a share your stats from this event pop up screen is displayed along with an infogrpahic - And copy the links required and use them - And click on cancel symbol - Then the user will be back on events page. diff --git a/features/04-Events/Stats/SocialMediaStats.feature b/features/04-Events/Stats/SocialMediaStats.feature deleted file mode 100644 index 47f448f3a5..0000000000 --- a/features/04-Events/Stats/SocialMediaStats.feature +++ /dev/null @@ -1,23 +0,0 @@ -Feature: Sharing social media friendly Stats - As a User (all roles) - In order to share the stats of a particular event or to social media platforms - I should be able to click on social media friendly stats button. - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -#when a group or host doesn't have a webiste, cannot share their stats. So, in order to share their stats social media friendly images of their stats are being created. - -Scenario: Sharing social media friendly images for their events impact stats - Given the user is a host or volunteere of the event - When a user clicks on the the share social media friendly stats button on the event page - Then they can see the images on a popup screen - And can share them. - -Scenario: Sharing social media friendly images for their group impact stats - Given the user is a member of the group - When a user clicks on the the share social media friendly stats button on the group page - Then they can see the images on a popup screen - And can share them. diff --git a/features/04-Events/UpcomingEvent_restarter.feature b/features/04-Events/UpcomingEvent_restarter.feature deleted file mode 100644 index 722a2557a6..0000000000 --- a/features/04-Events/UpcomingEvent_restarter.feature +++ /dev/null @@ -1,22 +0,0 @@ -Feature: Upcoming events details - As a restarter - In order to view the upcoming events - I should be able to navigate to the upcoming event page. - -Background: - Given the following account have been created as a restarter - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View upcoming events details - When a restarter wants to view the upcoming event details- event address, description, attendance - Then he can see on the upcoming event page. - -Scenario: Add to calendar - When a restarter wants to attend the party and wants add to calendar - Then click on add to calendar button - And the event will be added to your calendar. - -Scenario: Volunteer triggers RSVP button and sends email to host - When the volunteer clicks the RSVP button - Then the host would receive an email about status of the volunteer. \ No newline at end of file diff --git a/features/05-Groups/FindAndFollowGroups/FollowGroup.feature b/features/05-Groups/FindAndFollowGroups/FollowGroup.feature deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/features/05-Groups/FindAndFollowGroups/GroupDescription.feature b/features/05-Groups/FindAndFollowGroups/GroupDescription.feature deleted file mode 100644 index e297c4c2ee..0000000000 --- a/features/05-Groups/FindAndFollowGroups/GroupDescription.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: View a Group description - As a user (all roles) - In order to view a particular group description - I should be able to click on read more link on that particular group page - -Background: - Given the following account have been created as a restarter - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: To view description about group - When a restarter wants to know about a group and clicks on read more link - Then a pop screen appears with full description of the group. - - Scenario: Cancel the pop up screen - When a restarter wants to close the pop up screen and go back to that group page - Then he can click on cancel, will land on group page. diff --git a/features/05-Groups/FindAndFollowGroups/ViewAllGroups(admin).feature b/features/05-Groups/FindAndFollowGroups/ViewAllGroups(admin).feature deleted file mode 100644 index d30462d6ec..0000000000 --- a/features/05-Groups/FindAndFollowGroups/ViewAllGroups(admin).feature +++ /dev/null @@ -1,39 +0,0 @@ -Feature: View All Groups - As an admin - In order to view all the groups - I should be able to go to groups page and click on see all groups link - -Background: - Given the following account have been created as an admin - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View all groups - When an admin clicks on all groups link from the admin drop down - Then he lands on all groups page and can see all the groups - And can even search for group. - - Scenario: Search for groups - When an admin wants to search group, should enter the fields provided in the By details category as follows - | Name | Tag | Town/City | Country | - | dean12 | Example1 | London | UK | - | | | London | | - | | | | UK | - | | Example2 | | | - | James | | | | - And the fields here are optional, so can search by only country or only name etc., - Then the admin can view the filtered group. - -Scenario: Create new group - When a host wants to create a new group, should click on create new group button - Then add an group page opens. - -Scenario: To access group details - When a host wants to access/check the group details - And clicks on the group name link - Then host lands on that particular group page. - -Scenario: To check the restarters and hosts - When a host wants to check who are the hosts and restarters - And clicks on the number link under their respective category - Then host can view the details on a pop up screen. \ No newline at end of file diff --git a/features/05-Groups/FindAndFollowGroups/ViewAllGroups.feature b/features/05-Groups/FindAndFollowGroups/ViewAllGroups.feature deleted file mode 100644 index bdd1ea2010..0000000000 --- a/features/05-Groups/FindAndFollowGroups/ViewAllGroups.feature +++ /dev/null @@ -1,27 +0,0 @@ -Feature: View All Groups - As a User (host, restarter) - In order to view all the groups - I should be able to go to groups page and click on see all groups link - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View all groups - When a host clicks on see all groups link - Then he lands on all groups page and can see all the groups in that page. - -Scenario: Create new group - When a host wants to create a new group, should click on create new group button - Then add an group page opens. - -Scenario: To access group details - When a host wants to access/check the group details - And clicks on the group name link - Then host lands on that particular group page. - -Scenario: To check the restarters and hosts - When a host wants to check who are the hosts and restarters - And clicks on the number link under their respective category - Then host can view the details on a pop up screen. \ No newline at end of file diff --git a/features/05-Groups/FindAndFollowGroups/ViewGroup_admin.feature b/features/05-Groups/FindAndFollowGroups/ViewGroup_admin.feature deleted file mode 100644 index 52e86fd3f2..0000000000 --- a/features/05-Groups/FindAndFollowGroups/ViewGroup_admin.feature +++ /dev/null @@ -1,41 +0,0 @@ -Feature: View a Groups - As an admin - In order to view a particular group details - I should be able to go to groups page and click on a particular group link - -Background: - Given the following account have been created as an admin - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: To view the information about group - When an admin wants to know the information about a group - Then he can view on the particular group page - And can find all the info like key stats, device breakdown, environmental impact, upcoming evetns and recently completed events. - -Scenario: To navigate to other groups - When an admin wants to go to other group, he can click on Group name dropdown where other group names are present - Then he can easily navigate to other groups. - -Scenario: To view description about group - When an admin wants to know about a group - Then he can view under about the group section - And can even click on read more for more info about the group. - - Scenario: View Volunteers in group - When an admin wants to know the volunteers who are present in that group - Then he can view under volunteers section. - -Scenario: Add Volunteers in group - When an admin wants to add the volunteers in that group - Then he can click invite to group link under volunteers section. - -Scenario: Add event - When an admin wants to add an event - Then he can click on add event link - And can RSVP and can also add a device by clicking on respective links. - -Scenario: See all events - When an admin wants to see all the events that completed recently - Then he can click on see all events links - And can add a device by clicking on its link. \ No newline at end of file diff --git a/features/05-Groups/FindAndFollowGroups/ViewGroup_host.feature b/features/05-Groups/FindAndFollowGroups/ViewGroup_host.feature deleted file mode 100644 index b75727e819..0000000000 --- a/features/05-Groups/FindAndFollowGroups/ViewGroup_host.feature +++ /dev/null @@ -1,37 +0,0 @@ -Feature: View a Group - As a host - In order to view a particular group details - I should be able to go to groups page and click on a particular group link - -Background: - Given the following account have been created as a host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: To view information about the group - When a host wants to know the information about a group - Then he can view on the particular group page - And can find all the info like group address, website, key stats, device breakdown, environmental impact, upcoming events and recently completed events. - -Scenario: To view description about group - When a host wants to know about a group - Then he can view under about the group section - And can even click on read more for more info about the group. - - Scenario: View Volunteers in group - When a host wants to know the volunteers who are present in that group - Then he can view under volunteers section. - -Scenario: Add Volunteers in group - When a host wants to add the volunteers in that group - Then he can click invite to group link under volunteers section. - -Scenario: Add event - When a host wants to add an event - Then he can click on add event link - And can RSVP and can also add a device by clicking on respective links. - -Scenario: See all events - When a host wants to see all the events that completed recently - Then he can click on see all events links - And can add a device by clicking on its link. diff --git a/features/05-Groups/FindAndFollowGroups/ViewGroup_restarter.feature b/features/05-Groups/FindAndFollowGroups/ViewGroup_restarter.feature deleted file mode 100644 index 175d9be828..0000000000 --- a/features/05-Groups/FindAndFollowGroups/ViewGroup_restarter.feature +++ /dev/null @@ -1,41 +0,0 @@ -Feature: View a Group - As a restarter - In order to view a particular group details - I should be able to go to groups page and click on a particular group link - -Background: - Given the following account have been created as a restarter - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: To view information about the group - When a restarter wants to know the information about a group - Then he can view on the particular group page - And can find all the info like group address, website, key stats, device breakdown, environmental impact, upcoming events and recently completed events. - -Scenario: To view description about group - When a restarter wants to know about a group - Then he can view under about the group section - And can even click on read more for more info about the group. - - Scenario: View Volunteers in group - When a restarter wants to know the volunteers who are present in that group - Then he can view under volunteers section. - -Scenario: Join as a Volunteer in that group - When a restarter wants to join as a volunteer in that group - Then he can click on join gropu link under volunteers section. - -Scenario: View upcoming events - When a restarter wants to attend an event - Then he can click on RSVP link - And can add a device to an event which is happening by clicking on add a device link. - -Scenario: See all events - When a restarter wants to see all the events that completed recently - Then he can click on see all events links - And can add a device by clicking on its link. - -Scenario: Restarter triggers notification email to host by joining the group - When the restarter clicks on join group button - Then the host would receive an notification email about that restarter joining the group. \ No newline at end of file diff --git a/features/05-Groups/FindAndFollowGroups/YourGroups.feature b/features/05-Groups/FindAndFollowGroups/YourGroups.feature deleted file mode 100644 index 2c7240ae5e..0000000000 --- a/features/05-Groups/FindAndFollowGroups/YourGroups.feature +++ /dev/null @@ -1,32 +0,0 @@ -Feature: View Your Groups - As a User (All roles) - In order to view all the groups that a user is involved and other groups that are near user - I should be able to go to groups page - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View all groups -# View all gropus i.e., groups involved with and the groups that are near me - When a host clicks on group page - Then he lands on group page and can see all the groups in that page - And one section is the list of groups that host is involved - And other section is the list of groups that are near to the host along with a see all groups link. - -Scenario: Create new group - When a host wants to create a new group, should click on create new group button - Then add an group page opens. - -Scenario: To access group details - When a host wants to access/check the group details - And clicks on the group name link - Then host lands on that particular group page. - -Scenario: To check the restarters and hosts - When a host wants to check who are the hosts and restarters - And clicks on the number link under their respective category - Then host can view the details on a pop up screen. - -![](./images/invitation-email-not-on-platform.png) diff --git a/features/05-Groups/ManageGroup/AddAGroup.feature b/features/05-Groups/ManageGroup/AddAGroup.feature deleted file mode 100644 index b305ad600c..0000000000 --- a/features/05-Groups/ManageGroup/AddAGroup.feature +++ /dev/null @@ -1,38 +0,0 @@ -Feature: Add a group - As a User (Host, Admin) - In order to add a new group - I should be able to do by navigating to add group page - -Background: - Given the following account have been created as a host or an admin - | Email | Password | Role | - | hubert@planetexpress.com | hubert! | Admin | - | hermes@planetexpress.com | b3nd3r | Host | - | leela@planetexpress.com | l33l4 | NetworkCoordinator | - -Scenario: Create a new group - When a host clicks on add a group page and fills the data as follows - | Name of group | Your website | Tell us about your group | Group location | Group image | - | Mighty Restarters | https://mightyrestarters.co.uk | expert group in fixing things | Southwark | :) | - And clicks on create group button to create a new group - Then he lands on group page with the newly created group in the list of gropus in that page. - -Scenario: Amending the Area details of a group - When Leela or Hubert is adding an group - Then they see the section for adding the Area details of the group - -Scenario: Text cleaned in the description - When a host copies and paste into the description box - And the data should loose all htmls and css properties it has - Then it show a message inside description box as text cleaned. - -Scenario: How to give group location - When a host clicks on group location, types the address - Then automatically suggestions should show up and the place should be pointed in map. - -Scenario: searching the image -#TODO: when clicked on add group image here text, file explorer opens. - When user clicks on add image text, then file explorer should open - And browse for the image - And select the one needed - Then you will see the uploaded image thumbnail in that area. diff --git a/features/05-Groups/ManageGroup/AllMembersofGroup.feature b/features/05-Groups/ManageGroup/AllMembersofGroup.feature deleted file mode 100644 index 682e9a1dbd..0000000000 --- a/features/05-Groups/ManageGroup/AllMembersofGroup.feature +++ /dev/null @@ -1,14 +0,0 @@ -Feature: View all volunteers of a group - As a User (All roles) - In order to view all volunteers of a group - I should be able to click on Join group link. - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View all the volunteers of a group - When a user clicks on Join group from the group page - Then a pop up appears with all the list of restarters with their skills - And can click on join group button. \ No newline at end of file diff --git a/features/05-Groups/ManageGroup/BecomeAHost.feature b/features/05-Groups/ManageGroup/BecomeAHost.feature deleted file mode 100644 index 7508d9f125..0000000000 --- a/features/05-Groups/ManageGroup/BecomeAHost.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: Restarter becoming a host - As a restarter - In order to become a host - I should be able to create a new group by clicking on create new group button on group page - -Background: - Given the following account have been created as a restarter - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: To create a new group - When a restarter clicks on create new group - Then a pop up appears with message and a button to get started. - -Scenario: Cancel creating a group - When a restarter does not want to create a group and wants to go back to all groups page - Then he should click on cancel to go back. \ No newline at end of file diff --git a/features/05-Groups/ManageGroup/EditGroup.feature b/features/05-Groups/ManageGroup/EditGroup.feature deleted file mode 100644 index 93ddc029bc..0000000000 --- a/features/05-Groups/ManageGroup/EditGroup.feature +++ /dev/null @@ -1,40 +0,0 @@ -Feature: Edit a group - As a User (Host, Admin) - In order to edit a group - I should be able to do by navigating to edit group page - -Background: - Given the following account have been created as a host or an admin - | Email | Password | Role | - | dean@wecreatedigital.co.uk | dean | Host | - | hello@howareyou.com | hello | Admin | - -Scenario: Editing a group - When a host clicks on edit group page and edits the data as follows - | Name of group | Your website | Tell us about your group | Group location | Group image | Group tags| - | Mighty Restarter | https://mire.co.uk | experts in fixing things | Southwark | :) | Exampletag1 | - And clicks on approve group button to save the changes - Then he lands on group page with the edited group in the list of gropus in that page. - -Scenario: Text cleaned in the description - When a host copies and paste into the description box - And the data should loose all htmls and css properties it has - Then it show a message inside description box as text cleaned. - -Scenario: How to give group location - When a host clicks on group location, types the address - Then automatically suggestions should show up and the place should be pointed in map. - -Scenario: searching the image -#TODO: when clicked on add group image here text, file explorer opens. - When user clicks on add image text, then file explorer should open - And browse for the image - And select the one needed - Then you will see the uploaded image thumbnail in that area. - -Scenario: Adding group tags -#Only admin can add a new tag - When an admin clicks on add new tag link beside group tags and edits the data as follows - | Group tags| - | Exampletags1 | - Then the edited tag appers in the field with cancel option, if needed we can delete the tag using cancel option. \ No newline at end of file diff --git a/features/05-Groups/ManageGroup/InviteUsertoGroup.feature b/features/05-Groups/ManageGroup/InviteUsertoGroup.feature deleted file mode 100644 index d43327d4cb..0000000000 --- a/features/05-Groups/ManageGroup/InviteUsertoGroup.feature +++ /dev/null @@ -1,20 +0,0 @@ -Feature: Invite volunteers to a group by email - As a User (All roles) - In order to invite volunteers to a group - I should be able to click on invite to group link. - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Invite volunteers to a group - When a user clicks on Invite to group from the group page - Then a pop up appears, where email address and message should be entered as follows - | Email address | Message | - | dean@wecreatedigital.co.uk | Hi, Welcome! | - And can click on send invite button. - -Scenario: User triggers invitation to group email - When the user clicks the send invite to group button - Then the volunteer that the user has sent invite to group would receive an email about information of that group. \ No newline at end of file diff --git a/features/05-Groups/ManageGroup/InviteVolunteers_ToGroup_ByLink.feature b/features/05-Groups/ManageGroup/InviteVolunteers_ToGroup_ByLink.feature deleted file mode 100644 index 81fecc5a45..0000000000 --- a/features/05-Groups/ManageGroup/InviteVolunteers_ToGroup_ByLink.feature +++ /dev/null @@ -1,5 +0,0 @@ -Feature: Invite volunteers to a group by link - As a User (All roles) - In order to invite volunteers to a group - I should be able to click on 'invite to group' link to include in manual email. - \ No newline at end of file diff --git a/features/05-Groups/ManageGroup/PromoteGroupMemberToHost.feature b/features/05-Groups/ManageGroup/PromoteGroupMemberToHost.feature deleted file mode 100644 index 76732d7656..0000000000 --- a/features/05-Groups/ManageGroup/PromoteGroupMemberToHost.feature +++ /dev/null @@ -1,53 +0,0 @@ -Feature: Set group member as group host - -As a Host of a Group -In order to share the Host responsbilities -I need to be able to set other Group Members as a Host of the Group - -We need the ability for hosts to upgrade other members of the group to a host -role. Currently only the person who created the group is set as a host of the group. -Both hosts and admins should be able to set other members of the group as hosts -of the group. - -Both users with the Host and Restarter roles can be set as Hosts. If a Restarter is set -as a group host, their account should also be set to have the Host role. - -TODO: explain why we don't simply let being a Host and being a member of a group make you -a host of that group. - -TODO: worth noting that when a restarter becomes a host, it will change their dashboard. -Could potentially be confusing. I think we need a way of choosing what's on the dashboard. - -Scenario: Host of a group sets a group member with the host role as a group host - Given I am host of a group - When I set another group member who has the host role to be group host - Then they are set as a host of the group -# Fail -# Error when viewing a group -# Undefined variable: formdata (View: /home/neil/Code/fixometer_laravel/resources/views/partials/volunteer-row.blade.php) (View: /home/neil/Code/fixometer_laravel/resources/views/partials/volunteer-row.blade.php) (View: /home/neil/Code/fixometer_laravel/resources/views/partials/volunteer-row.blade.php) - -Scenario: Host of a group sets a group member with the restarter role as a group host - Given I am host of a group - When I set another group member who has the Restarter role to be group host - Then they are set as a host of the group - And they are given the host role -# Fail -# Error when viewing a group, as above - -Scenario: Admin sets a group member with the host role as a group host - Given I am an Admin - When I set a group member of a group who has the host role to be group host - Then they are set as a host of the group -# Pass - -Scenario: Admin sets a group member with the restarter role as a group host - Given I am host of a group - When I set a group member of a group who has the host role to be group host - Then they are set as a host of the group - And they are given the host role -# Pass - - -# General Fail -# The action of marking as a host succeeded, but received an error -# 'Trying to get property 'name' of non-object' - think this is related to the user not having opted in to email notifications \ No newline at end of file diff --git a/features/05-Groups/ManageGroup/RemoveVolunteerFromGroup.feature b/features/05-Groups/ManageGroup/RemoveVolunteerFromGroup.feature deleted file mode 100644 index cb2efb9159..0000000000 --- a/features/05-Groups/ManageGroup/RemoveVolunteerFromGroup.feature +++ /dev/null @@ -1,36 +0,0 @@ -Feature: Remove volunteer from group - -Hosts need to be able to remove volunteers from groups. They might be a long-standing -member who is no longer active with the group, or sometimes it is just that someone has -been added to the group by mistake. - -Only Hosts of a group and Admins should be able to remove members from a group. - -TODO: should there be any associated notifications with this? - -Scenario: Admin removes member from a group they're a member of - Given I am an Admin - When I remove a member from a group I'm a member of - Then the member is no longer part of the group - And I see a message confirming that the member has been removed successfully -# Fail -# No message shown upon successful removal - -Scenario: Admin removes member from a group they're not a member of - Given I am an Admin - When I remove a member from a group I'm not a member of - Then the member is no longer part of the group - And I see a message confirming that the member has been removed successfully -# Fail -# No message shown upon successful removal - -Scenario: Host removes member from a group - Given I am an Host - When I remove a member from a group - Then the member is no longer part of the group -# Fail -# See General Fail - -# General Fail -# Can't view groups -# Undefined variable: formdata (View: /home/neil/Code/fixometer_laravel/resources/views/partials/volunteer-row.blade.php) (View: /home/neil/Code/fixometer_laravel/resources/views/partials/volunteer-row.blade.php) (View: /home/neil/Code/fixometer_laravel/resources/views/partials/volunteer-row.blade.php) \ No newline at end of file diff --git a/features/05-Groups/ManageGroup/StatsEmbed.feature b/features/05-Groups/ManageGroup/StatsEmbed.feature deleted file mode 100644 index 3335dbb768..0000000000 --- a/features/05-Groups/ManageGroup/StatsEmbed.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: Share your Stats - As a User (all roles) - In order to share the stats of a particular event to other place - I should be able to click on Events stats embed button on group page. - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Sharing your stats - When a user wants to share their group stats to other places, click on Group stats embed button - And a pop up appreas with iframe as headline stats and CO2 equivalence visualisation - And copy the links required and use them - And click on cancel symbol - And preview widget link is useful for how the iframe looks visulally on screen - Then the user will be back on group page. \ No newline at end of file diff --git a/features/06-Fixometer/AddData.feature b/features/06-Fixometer/AddData.feature deleted file mode 100644 index 5f7e3f2ce7..0000000000 --- a/features/06-Fixometer/AddData.feature +++ /dev/null @@ -1,15 +0,0 @@ -Feature: Add data quickly from the Fixometer - -In order to easily the amount of data collected, -As an admin or host, -I want to quickly add data from the Fixometer section. - -Scenario: someone who should be able to add data - Given I am a host or admin, or a repairer who has been to an event - When I visit the Fixometer page - Then I see the Add Data button - -Scenario: someone who shouldn't be able to add data - Given I'm a repairer who has never been to an event - When I visit the Fixometer page - Then I don't see the Add Data button diff --git a/features/06-Fixometer/EditDevice.feature b/features/06-Fixometer/EditDevice.feature deleted file mode 100644 index 7cf7198ba7..0000000000 --- a/features/06-Fixometer/EditDevice.feature +++ /dev/null @@ -1,22 +0,0 @@ -Feature: Edit devices - As a user (all roles) - In order to edit the devices - I should be able to navigate edit devices page. - -Background: - Given the following account have been created a restarter - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Edit devices - When a restarter clicks on edit devices page - And change/update the fields as follows - | Resatart party | Category of device | Brand | Model | Age | Add devices image here | Repair Status | Repair information | Spare parts required | Description of problem/solution | Suitable for community | - | Restart HQ | Paper shredder | Amazon | Basics HY245 | 3 years | | Repairable | Do it yourself | yes | fuse blown out | tick symbol | - And click on save device to save the changes - Then you will land on all devices page with the edited device on the list of devices. - -Scenario: delete device - When a restarter wants to delete a device, click on delete device button - Then you will land on all devices page and you won't be able to see the deleted device from the list of devices. - diff --git a/features/06-Fixometer/FilterItems.feature b/features/06-Fixometer/FilterItems.feature deleted file mode 100644 index 3d9aa6faf9..0000000000 --- a/features/06-Fixometer/FilterItems.feature +++ /dev/null @@ -1,12 +0,0 @@ -Feature: Filtering items - -Scenario: Searching by Item & Repair Info - Given I have expanded the Item & Repair Info filter group - Then I see Category, Brand, Model or Item Type, Repair Status, ‘Search problem/solution’, and Interesting Case Study - -Scenario: Filtering by category - Given I have clicked the category selector - Then I see the list of clusters with the categories per cluster - And a new artificial cluster title is introduced called ‘Other (powered)' with the sole category being ‘Misc (powered)' - this cluster appears above the 'Non-powered items’ cluster - -Scenario: Searching by Item & Repair Info diff --git a/features/06-Fixometer/ViewAllItems.feature b/features/06-Fixometer/ViewAllItems.feature deleted file mode 100644 index a9ca4cb326..0000000000 --- a/features/06-Fixometer/ViewAllItems.feature +++ /dev/null @@ -1,22 +0,0 @@ -Feature: View list of items - -Background: - Given I am any user, looking at the Repair Records section of the Fixometer - -Scenario: View all items - Then I can see a tab for powered items and a tab for unpowered items - And for powered items I can see 'Category', 'Brand', 'Model', 'Assessment', 'Group', 'Status', and 'Date' - And for unpowered items I can see 'Category', 'Item Type', 'Assessment', 'Group', 'Status', and 'Date' - And the items are ordered by date descending - -Scenario: Expanding result - When I click on an item then it expands to display the expanded view of the item as per the designs - -Scenario: Pagination - When I click on a page number in either the powered or unpowered tab - Then the table advances to that page within that tab - -Scenario: Sorting - When I click on one of the column headings - Then the ordering of the table is ordered by that column (cycling through ascending and descending) - diff --git a/features/06-Fixometer/ViewGlobalImpact.feature b/features/06-Fixometer/ViewGlobalImpact.feature deleted file mode 100644 index b4baf650d1..0000000000 --- a/features/06-Fixometer/ViewGlobalImpact.feature +++ /dev/null @@ -1,28 +0,0 @@ -Feature: View global impact - -In order to feel enthused about the importance of repair -As a repair volunteer -I want to see the global impact of repairs within Restarters - -Background: - Given I am viewing the Fixometer page - # Navigate to https://restarters.dev/fixometer - -Scenario: Stats boxes - Then a CO2 emissions stats box shows the CO2 prevented for powered items only - And the consumption figure shows the equivalent consumption of km driven - And a stats box for waste prevented shows waste prevented for powered and unpowered items - And a stats box for powered items shows the number of repaired powered items - And a stats box for unpowered items shows the number of repaired unpowered items - And a stats box for participants shows the total number of participants - And a stats box for hours volunteered shows the total number of hours volunteered - -Scenario: - Given a group has added items to the most recent event in the Fixometer - Then the waste weight prevented by the event's fixed items is displayed in the Latest Data card - And the figure includes both unpowered and powered items - And the text says ' just prevented X kg of waste!' - And clicking the group name goes to the group - And clicking the figure for the weight goes to the event at which the waste was prevented - -![](./our-global-impact.png) diff --git a/features/06-Fixometer/our-global-impact.png b/features/06-Fixometer/our-global-impact.png deleted file mode 100644 index 37efedc933..0000000000 Binary files a/features/06-Fixometer/our-global-impact.png and /dev/null differ diff --git a/features/07-Reporting/BreakdownbyCountry.feature b/features/07-Reporting/BreakdownbyCountry.feature deleted file mode 100644 index 21c542df57..0000000000 --- a/features/07-Reporting/BreakdownbyCountry.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: Breakdown by country - Total hours volunteered - As a user (all roles) - In order to see the total time volunteered country wise - I should be able to click on see all results link in breakdown by country section on time volunteered page. - -Background: - Given the following account have been created a restarter - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Total time volunteered country wise - When a restarter wants to see the total time volunteered country wise, click on see all results link in breakdown by country section - Then a pop up appears with all the country names and the time volunteered in the countries. - -Scenario: Click on cancel - When a restarter wants to go back to time volunteered page, click on Cancel - Then the restarter will go back to time volunteered page. \ No newline at end of file diff --git a/features/07-Reporting/Impact Analysis.feature b/features/07-Reporting/Impact Analysis.feature deleted file mode 100644 index ad8bff8cca..0000000000 --- a/features/07-Reporting/Impact Analysis.feature +++ /dev/null @@ -1,36 +0,0 @@ -Feature: Group Impact Analysis - As a Host - In order to be able to showcase the work of my Group - I want to see an impact analysis of my Group's Events - -Group hosts are able to get reports about the impact of individual Restart -Parties: total of waste prevented, CO2 emissions prevented, hours -volunteered. - -Group hosts are also able to get an an aggregate total of all waste -prevented, CO2 emissions prevented, hours volunteered for the -entirety of their group’s events. - -TODO: Add larger dataset test and checking the algorithms. - -Background: - Given the following groups: - | Id | Name | - | 1 | Hackney Fixers | - And the following events: - | Id | Location | Date | - | 1 | The Redmond Centre, Manor House | 28/01/2017 | - | 2 | Homerton Library | 19/11/2016 | - -Scenario: Impact analysis for event - Given the following devices logged for the 'Homerton Library' event: - | Id | Category | Comment | Brand | Model | Repair Status | Spare Parts? | - | 1 | Laptop medium | Needs new screen | Apple | Mac Air | Repairable | Yes | - When viewing the stats for the 'Homerton Library' event - Then the stats should be: - | Participants | Restarters | CO2 Emissions Prevented | Fixed | Repairable | Dead | - | 35 | 5 | 914kg | 12 | 21 | 5 | - -Scenario: Impact analysis for group - When viewing the stats for the 'name of the group' group - Then the stats should be: \ No newline at end of file diff --git a/features/07-Reporting/TimeVolunteered.feature b/features/07-Reporting/TimeVolunteered.feature deleted file mode 100644 index a544ff0cb1..0000000000 --- a/features/07-Reporting/TimeVolunteered.feature +++ /dev/null @@ -1,21 +0,0 @@ -Feature: Total hours volunteered - As a user (all roles) - In order to see the total time volunteered - I should be able to navigate to time volunteered page. - -Background: - Given the following account have been created a restarter - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Total time volunteered - When a restarter wants to see the total time volunteered - Then he can see all the information about volunteered time on time volunteered page. - -Scenario: search for a paticular period of time volunteered -#Users can search for devices by either taxonomy or by date or by users or by location and miscellaneous. - When a restarter wants to search for a particular period of time volunteered, he can fill the fields as he want to search as follows - | Group | Group tag | Name | Age range | Gender | From date | To date | Country | Region | Include anonymous users | - | Restart HQ | Exampletag1 | James | 23-28 | Male | 23/04/2017 | 12/08/2017 | UK | London | Yes | - And should click on search all time volunteered - Then user can view the list of time volunteered. \ No newline at end of file diff --git a/features/08-Admin/Brands/AddBrand.feature b/features/08-Admin/Brands/AddBrand.feature deleted file mode 100644 index 1d7a8c4072..0000000000 --- a/features/08-Admin/Brands/AddBrand.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: Add Brand - As an admin - In order to add a brand - I should be able to do that by filling the fields of add brand pop-up and click on create new brand button - -Scenario: Adding new brand - When a new brand name is added, to do so fill the field as follows and Click on Create new brand button to save the changes - | Brand name | - | TP-Link | - Then you will land on All brands page with newly added brand in the list and also with a message that your brand is added. \ No newline at end of file diff --git a/features/08-Admin/Brands/EditBrand.feature b/features/08-Admin/Brands/EditBrand.feature deleted file mode 100644 index 19b15ca4f0..0000000000 --- a/features/08-Admin/Brands/EditBrand.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: Edit Brand Name - As an admin - In order to edit brand name - I should go to edit brand page and click on save brand to save the changes - -Scenario: Editing a brand name - When a brand name is edited, should edit the field as follows and click on save brand button to save the changes - | Brand name | - | HP | - Then she will land on brands name page with the edited brand name in the list, pop-up message saying your changes have beeen saved. \ No newline at end of file diff --git a/features/08-Admin/Brands/ViewAllBrands.feature b/features/08-Admin/Brands/ViewAllBrands.feature deleted file mode 100644 index 55055c4528..0000000000 --- a/features/08-Admin/Brands/ViewAllBrands.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: Brand name - As an admin - In order to view all the brand names in one page - I should be able to do that by navigating to brand page - -Background: - Given the following account have been created as an admin/user - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View All Brands - When an admin navigate to Brand page - Then she can view all the brand names in that page. - -Scenario: Creating new brand - When an admin wants to create a new brand - Then he/she should click on create new brand button. \ No newline at end of file diff --git a/features/08-Admin/Categories/EditCategory.feature b/features/08-Admin/Categories/EditCategory.feature deleted file mode 100644 index 8172397c79..0000000000 --- a/features/08-Admin/Categories/EditCategory.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: Edit Category - As a User or an Admin - In order to change the details of category - I should be able to do by using a edit category page - -Background: - Given the following account have been created as a user or an admin - | Email | Password | - | jenny@google.co.uk | dean1 | - -Scenario: Edit Category - When the fields are changed/updated in edit category section as follows - | Category name | weight(kg) | CO2 Footprint(kg) | Reliability | Category cluster | Description | - | jenny@google.co.uk | dean1 | 1.34 | good | Digital telephone | good product | - And click on save category - Then she will land on All categories page with the edited category in the list of categories. \ No newline at end of file diff --git a/features/08-Admin/Categories/ViewAllCategories.feature b/features/08-Admin/Categories/ViewAllCategories.feature deleted file mode 100644 index bf331814e8..0000000000 --- a/features/08-Admin/Categories/ViewAllCategories.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: All Categories - As an admin - In order to view all categories - I should be able to navigating on the Categories page - -Scenario: View All Categories - When an admin view all the categories - Then he/she should navigate to categories page. \ No newline at end of file diff --git a/features/08-Admin/GroupTags/AddNewGroupTag.feature b/features/08-Admin/GroupTags/AddNewGroupTag.feature deleted file mode 100644 index 84d6b429d2..0000000000 --- a/features/08-Admin/GroupTags/AddNewGroupTag.feature +++ /dev/null @@ -1,12 +0,0 @@ -Feature: Add new group tag - As an admin - In order to add a new group tag - I should fill the fields of add new group tag pop-up and click on create new tag button - -Scenario: Adding new group tag - When the fields are added as follows - | Tag name | Description(optional | - | Example tag1 | | - | Example tag2 | | - And should click on Create new tag button to save the changes - Then she should land on group tag page with the recently added group tag in list of tags. \ No newline at end of file diff --git a/features/08-Admin/GroupTags/EditGroupTag.feature b/features/08-Admin/GroupTags/EditGroupTag.feature deleted file mode 100644 index c18291dc86..0000000000 --- a/features/08-Admin/GroupTags/EditGroupTag.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: Edit group tag - As an admin - In order to add edit group tag - I should navigate to edit group tag page and click on save tag to save the changes - -Scenario: Editing a group tag - When the fields are editted as follows - | Tag name | Description(optional | - | Example tag12 | | - | Example tag2 | | - And should click on save tag button to save the changes - Then she should land on group tags page with the edited tag in the list of tags. - -Scenario: Deleting a group tag - When an admin wants to delete a group tag - And click on delete tag button to delete the group tag - Then she should land on group tags pages with no trace of the deleted tag in the list. \ No newline at end of file diff --git a/features/08-Admin/GroupTags/ViewAllGroupTags.feature b/features/08-Admin/GroupTags/ViewAllGroupTags.feature deleted file mode 100644 index bf35d6456e..0000000000 --- a/features/08-Admin/GroupTags/ViewAllGroupTags.feature +++ /dev/null @@ -1,12 +0,0 @@ -Feature: All group tags - As an admin - In order to see all the group tags - I should navigate to group tags page - -Scenario: View All Group Tags - When an admin wants to see all the group tags at one place - Then she should navigate to Group Tags page. - -Scenario: Creating new tag - When an admin wanted to create a new group tag - Then he/she should click on create new tag button. \ No newline at end of file diff --git a/features/08-Admin/Roles/EditRole.feature b/features/08-Admin/Roles/EditRole.feature deleted file mode 100644 index f7a38a1875..0000000000 --- a/features/08-Admin/Roles/EditRole.feature +++ /dev/null @@ -1,14 +0,0 @@ -Feature: Edit Role - As an admin - In order to create/edit/delete a user and to create a party - I should be able to do on an edit page and saving the changes through save role button - -Background: - Given the following account have been created as an admin - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Editing User Role - When the user permission checked - And the user will have those permissions to do and click on save role to save the changes - Then she should land on All users page with the edited user in the list of users. \ No newline at end of file diff --git a/features/08-Admin/Roles/ViewAllRoles.feature b/features/08-Admin/Roles/ViewAllRoles.feature deleted file mode 100644 index 0465917b89..0000000000 --- a/features/08-Admin/Roles/ViewAllRoles.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Roles - As a Admin - In order to view the roles - I should be able to see them with permissions - -Scenario: Roles with Permissions - When an admin wants to view the permissions of the Roles - Then navigate to roles page \ No newline at end of file diff --git a/features/08-Admin/Skills/AddNewSkill.feature b/features/08-Admin/Skills/AddNewSkill.feature deleted file mode 100644 index 538115678b..0000000000 --- a/features/08-Admin/Skills/AddNewSkill.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: Add new skill - As an admin - In order to add a new skill - I should fill the fields of add new skill pop-up and click on create new skill button - -Background: - Given the following account have been created as an admin - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Adding new skill - When a admin needs new skill to their profile, they should fill the fields as follows - | Skill name | Description(optional | - | Mobile devices | | - | laptops | | - And click on Create new skill button to save the changes - Then she should land on all skills page with the new skill added in the list of skills, with a message saying new skill have been added. diff --git a/features/08-Admin/Skills/EditSkill.feature b/features/08-Admin/Skills/EditSkill.feature deleted file mode 100644 index c74deea273..0000000000 --- a/features/08-Admin/Skills/EditSkill.feature +++ /dev/null @@ -1,22 +0,0 @@ -Feature: Edit skill - As an admin - In order to add edit my skill - I should navigate to edit skill page and click on save skill to save the changes - -Background: - Given the following account have been created as an admin/user - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Editing a skill - When an admin edit a skill which is in their profile, they should edit the fields as follows - | Skill name | Description(optional | - | Scanners | | - | laptops | | - And click on save skill button to save the changes - Then she will land on all skills page with the edited skill in the list of skills, with a message saying your changes have been saved. - -Scenario: Deleting a skill - When an admin wants to delete a skill which is in their profile - And click on delete skill button to delete the skill - Then she will land on all skills page where the deleted skill will no longer be there in the list of skills, with a message saying your skill have been deleted. diff --git a/features/08-Admin/Skills/ViewAllSkills.feature b/features/08-Admin/Skills/ViewAllSkills.feature deleted file mode 100644 index 8b4c282f07..0000000000 --- a/features/08-Admin/Skills/ViewAllSkills.feature +++ /dev/null @@ -1,18 +0,0 @@ -Feature: All Skills - As a Admin - In order to know the skills - I should be able to see them with description - -Background: - Given the following account have been created as a admin - | Email | Password | - | jenny@google.co.uk | dean1 | - | dean@google.co.uk | helo1 | - -Scenario: View All skills with description - When an admin wants to know the description of skills - Then they can navigate to the Skills page. - -Scenario: Create new skill button - When an admin wants to add a skill to their profile - Then click on create new skill button and follow the steps. diff --git a/features/08-Admin/Users/AddNewUser.feature b/features/08-Admin/Users/AddNewUser.feature deleted file mode 100644 index 99e6046df1..0000000000 --- a/features/08-Admin/Users/AddNewUser.feature +++ /dev/null @@ -1,29 +0,0 @@ -Feature: Add New User - As an Admin - In order to create new user accounts for cases when the user can't self-register - I would like a Create New User facility. - -Scenario: Creating new users - Given an Admin user is on the All Users page - When she clicks the New User button - Then she is shown the dialog for creating the new user - -Scenario: Valid details added for user -# Entering correct details in the fields provided at Add new user pop-up. - Given an Admin is creating a new user - When she enters the new user's details in the fields provided as follows: - | Name | Email address | User role | Password | Repeat password | - | diamond | diamond@gmail.com | Volunteer | h£!!05 | h£!!05 | - | james | james@yahoo.com | Restarter | scr7vd* | scr7vd* | - And she clicks 'Create new user' - Then she lands on the All Users page with the newly added user in the list of users - And she is shown a message saying that new user has been added successfully - -Scenario: Invalid details added for user -# Entering invalid details in the fields provided at Add new user pop-up. - Given an Admin is creating a new user - When she enters the new user's details in the fields provided as follows: - | Name | Email address | User role | Password | Repeat password | - | diamond | diamond@gmail.com | Volunteer | h£!! | h£!! | - And she clicks 'Create new user' - Then an error message should at the password field, password should be more than 6 characters. \ No newline at end of file diff --git a/features/08-Admin/Users/DeleteUser.feature b/features/08-Admin/Users/DeleteUser.feature deleted file mode 100644 index 152a23a2a4..0000000000 --- a/features/08-Admin/Users/DeleteUser.feature +++ /dev/null @@ -1,13 +0,0 @@ -Feature: Delete User - - As a user - In order to exercise my right to be forgotten - I would like to be able to delete my account - -Scenario: Admin deletes user's account - Given an Admin is on a user's account page - When she deletes the users account - Then the user's personal data is anonymised - And the account is marked as inactive - And the Admin is directed to the All Users page - And the Admin is shown a message showing that this user has been successfully deleted \ No newline at end of file diff --git a/features/08-Admin/Users/EditUser.feature b/features/08-Admin/Users/EditUser.feature deleted file mode 100644 index 034a67cf49..0000000000 --- a/features/08-Admin/Users/EditUser.feature +++ /dev/null @@ -1,39 +0,0 @@ -Feature: Edit user (Profile) - As an Admin - In order to change the details entered before - I should be using a edit user functionality - -Background: - Given the following account have been created as a user - | Email | Password | - | jenny@google.co.uk | dean1 | - -Scenario: Edit User - When a user wants to change/update any details - And he/she should be able to do that by changing the details and saving them - Then she should land on the Users page with the edited user in the list of users, a message saying that the changes have been saved . - -Scenario: Editing User Profile -# Updating details in the User Profile section and click on save profile button - When a user enter details in User Profile section as follows and clicks on save profile - | Name | Email address | Age | Country | Town/City | Gender | Your biography(optional) | - | jenny | jenny@gmail.com | 45 | United Kingdom | Remakery | Male | I am an Artist by proffesion | - | diamond | diamond@gmail.com | 23 | Spain | Belgium | Male | | - And the user saves all the changes he made in that section - Then she should land on the profile page with a message saying that the changes have been saved. - -Scenario: Editing Repair Skills -# Updating details in the Repair skills section, only prefixed skills in the system are saved - When a user types the skills he/she have - | Key Skills | - | Mobiles devices | - | Laptops | - | Kitchen devices | - And the user saves the changes in that section - Then she should land on the profile page with a message saying that the changes have been saved. - -Scenario: Upload profile picture -# Updating the profile picture in change photo section - When a user wants to change their profile picture - And browse the pic and click on change photo button - Then she should land on profile page with the uploaded picture in the placeholder, with a message saying the picture has been uploaded. \ No newline at end of file diff --git a/features/08-Admin/Users/EditUser_Acc.feature b/features/08-Admin/Users/EditUser_Acc.feature deleted file mode 100644 index 8c0106c659..0000000000 --- a/features/08-Admin/Users/EditUser_Acc.feature +++ /dev/null @@ -1,21 +0,0 @@ -Feature: Edit user (Account) - As an Admin - In order to change the account details entered before - I should use edit functionality - -Scenario: Edit User account - When an admin changes/updates any account details and clicks on save - Then he/she should see an pop up message as changes have been saved. - -Scenario: Changing Password -# Change password and click on change password button to save - When changes are made in the fields as follows and clicks on change password button - | Current password | New password | New repeat password | - | jenny | hello! | hello! | - | diamond | hi£donna! | hi£donna! | - Then a pop-up message shows saying all the changes have been saved. - -Scenario: Admin only -# Updating details in the Repair skills section - When the admin uses this page to change a users role and group - Then only admin can have that privilage to do. \ No newline at end of file diff --git a/features/08-Admin/Users/EditUser_Emailpref.feature b/features/08-Admin/Users/EditUser_Emailpref.feature deleted file mode 100644 index 4bfea91413..0000000000 --- a/features/08-Admin/Users/EditUser_Emailpref.feature +++ /dev/null @@ -1,19 +0,0 @@ -Feature: Email Preferences - As an Admin - In order to get notified by the Restart Project - I should signup for email alerts and save the preferences - -Background: - Given the following account have been created as a user - | Email | Password | - | jenny@google.co.uk | dean1 | - -Scenario: Check Email preferences - When an admin wants to get notified by the Restart Project - And ticking-off the checkbox and click on save preferences button - Then she should land on Email preferences page with a message saying that the changes have been saved. - -Scenario: Creating an email. -# User can create a email or set an email to Restart Project discussion platform and click on save preferences button - When a user create a email or set an email to Restart Project discussion platform - Then the user receives the information to that email id \ No newline at end of file diff --git a/features/08-Admin/Users/RepairDirectoryAccess.feature b/features/08-Admin/Users/RepairDirectoryAccess.feature deleted file mode 100644 index 41d217b39c..0000000000 --- a/features/08-Admin/Users/RepairDirectoryAccess.feature +++ /dev/null @@ -1,31 +0,0 @@ -Feature: Access to Repair Directory Admin - -Only certain users should have a menu item that links through to the Repair Directory Admin section. -This is a user-level permission (not role-based) that can only be set by an Admin. - -Scenario: Admin can set repair directory admin permission for another user - Given I am logged in as an Admin - When I visit the user account page for another user - Then I should see the permissions section for setting repair directory admin access - -Scenario: Host cannot set repair directory admin permission for themselves - Given I am logged in as a Host - When I visit my account editing page - Then I should not see the permissions section for setting repair directory admin access - -Scenario: Admin sets repair directory admin permission for themself - Given I am logged in as an Admin - When I visit the user account page for another user - And I set the Repair Directory Link permission on the user - Then the user should now have Repair Directory Link permission - And should see the Repair Directory menu item in the top left menu -# Pass - -Scenario: Admin sets repair directory admin permission for another user - Given I am logged in as an Admin - When I visit the user account page for another user - And I set the Repair Directory Link permission on the user - Then the user should now have Repair Directory Link permission - And should see the Repair Directory menu item in the top left menu -# Fail -# It doesn't persist the change to the setting when it's for another user \ No newline at end of file diff --git a/features/08-Admin/Users/ViewAllUsers.feature b/features/08-Admin/Users/ViewAllUsers.feature deleted file mode 100644 index b888578e64..0000000000 --- a/features/08-Admin/Users/ViewAllUsers.feature +++ /dev/null @@ -1,44 +0,0 @@ -Feature: Search - All users - As an admin - In order to see all users or search for a particular user - I should be able to do by using a search button - -Background: - Given the following account have been created as an admin - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -# Valid search - Multiple scenario's -Scenario: Entering details in all the fields -# Entering correct details in the fields provided at By details category. - When an admin enter details of a particular user in the feilds provided as follows - | Name | Email | Town/City | Group | Role | - | jenny | jenny@gmail.com | London | Remakery | Host | - | diamond | diamond@gmail.com | Belgium | Group2 | Volunteer | - | james | james@yahoo.com | Portsmouth | Group3 | Restarter | - Then the admin should get the details of that particalr user. - -Scenario: Entering details in only one field like name or email -# Entering correct details in the fields provided at By details category. - When an admin enter details of a particular user in the feilds provided as follows - | Name | Email | Town/City | Group | Role | - | jenny | | | | | - | | diamond@gmail.com | | | | - Then the admin should get the details of that particalr user. - -Scenario: Entering details in any two of the fields like name and Town/City or Email and Role or name and group -# Entering correct details in the fields provided at By details category. - When an admin enter details of a particular user in the feilds provided as follows - | Name | Email | Town/City | Group | Role | - | jenny | | London | | | - | | diamond@gmail.com | | | Volunteer | - | james | | | Group3 | | - Then the admin should get the details of that particalr user. - -Scenario: Invalid Search -# Not entering any of the fields and clicking on search all users button. - When an admin does not enter any field as follows - | Name | Email | Town/City | Group | Role | - | | | | | | - And clicks on search users button - Then she will land on All users page without any changes. \ No newline at end of file diff --git a/features/08-Admin/Users/ViewProfile.feature b/features/08-Admin/Users/ViewProfile.feature deleted file mode 100644 index dde1de182f..0000000000 --- a/features/08-Admin/Users/ViewProfile.feature +++ /dev/null @@ -1,15 +0,0 @@ -@current -Feature: View profile of an User - As a User (All roles) - In to see the profile of the user - I should be able to see on view profile page. - -Scenario: View profile page - Given I am logged in - When a user wants to see the biography and skills of a user and click on view profile - Then they will land on view profile page with their details. - -Scenario: Edit User - Given I am logged in - When user wants to change the profile, click on edit profile button - Then user will land on edit profile page. \ No newline at end of file diff --git a/features/09-Notifications-and-emails/Emails.feature b/features/09-Notifications-and-emails/Emails.feature deleted file mode 100644 index 659e195d5a..0000000000 --- a/features/09-Notifications-and-emails/Emails.feature +++ /dev/null @@ -1,84 +0,0 @@ -Feature: Emails that are sent out by the system - As a user (all roles) - In order to organise the platform - I should be able to send automated/manual emails to users whenever required. - -Background: - Given the following account have been created an admin - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: Post event automated reminder email to host (4) - Given a host has received a post event automated email - When the host clicks on Contribute data button - Then the host lands on that manage event page. - -Scenario: Post event reminder email to volunteers (5) - Given a volunteer has received a post event device reminder email - When the volunteer clicks on Contribute data button - Then the volunteer land on that manage event page. - -Scenario: Password reset request email (1) - Given a user has received a password reset request email - When the user clicks on Reset password button - Then the user land on the password rest page. - -Scenario: Email invitation to group by a user to a existing volunteer (2) - Given a existing volunteer has received an email invitation to group - When the existing volunteer clicks on Join group button - Then the volunteer land on that event page with a successful message on top. - -Scenario: Email invitation to group by a user to a new volunteer (2) - Given a new volunteer has received an email invitation to group - When the new volunteer clicks on Join group button - Then the volunteer land on registeration page, go through registration process - And should land on that group page with a welcome message. - - Scenario: Email invitation to an event by existing user to a existing volunteer (3) - Given existing volunteer has received an email invitation to an event - When the existing volunteer clicks on Read more button - Then the volunteer land on that particular event page. - - Scenario: Email Notification about event creation to admin (9) - Given admin has received an email notification about an event to moderate - When the admin clicks on View event button - Then the admin land on that particular edit event page, clicks on approve button. - - Scenario: Email Notification about group creation to admin (missing wireframe) - Given admin has received an email notification about a group has been created - When the admin clicks on View group button - Then the admin land on that particular group page. - - Scenario: Account created by admin to a new volunteer (13) - Given a new volunteer has received an email to set password - When the new volunteer clicks on Set password button - Then the volunteer land on password reset page. - -Scenario: Email notification to admin by a host/restarter when description of a repair has been marked suitable for wiki (12) - Given admin has received an email notification about repair description to wiki - When the admin clicks on View repair notes button - Then the admin land on edit device page. - -Scenario: Email notification to host by admin when event has been approved (10) - Given host has received an email notification about event confirmation - When the host clicks on View event button - Then the host land on Upcoming event page or therestartproject.org(if clicked the link in the email). - -Scenario: Email notification to host by new volunteer when he joined the group (8) - Given host has received an email notification about a volunteer joined the group - When the host clicks on Go to group button - Then the host land on that view group page. - -Scenario: Email notification to host by volunteer when he has sent an RSVP (7) - Given host has received an email notification about a volunteer attending the event - When the host clicks on View your event button - Then the host land on upcoming event page. - -Scenario: Admin can select the type of emails he/she would like to receive - Given admin wants to select the type of emails - When the user clicks on the checkboxes in the preferences section - Then the user wil get emails accordingly. - -Scenario: Admin receives email when abnormal number of Misc devices are added - When user enters abnormal number of misc devices - Then the admin gets an email about the scenario. \ No newline at end of file diff --git a/features/09-Notifications-and-emails/NewGroupCreatedNearby.feature b/features/09-Notifications-and-emails/NewGroupCreatedNearby.feature deleted file mode 100644 index 1540ad0581..0000000000 --- a/features/09-Notifications-and-emails/NewGroupCreatedNearby.feature +++ /dev/null @@ -1,11 +0,0 @@ -Feature: Notification of new group created nearby - -As an unaffiliated volunteer -In order to find repair groups that I can volunteer with -I want to be notified when a new group is created near me - -When a new group is created, volunteers within 25 miles of the newly created group are sent a notification, to let them know that the new group has been created. - -If the volunteer has opted in to receive email notifications, they will also be sent an email notification. - -![](./images/email__new-group-near-you.png) diff --git a/features/09-Notifications-and-emails/Notifications.feature b/features/09-Notifications-and-emails/Notifications.feature deleted file mode 100644 index dee9165e2e..0000000000 --- a/features/09-Notifications-and-emails/Notifications.feature +++ /dev/null @@ -1,47 +0,0 @@ -Feature: View Notifications - As a User (All roles) - In order to view all the notifications - I should be able to click on notification symbol with viewing notifications in it. - -Background: - Given the following account have been created as an host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario: View all notifications -# View all events i.e., notifications related to events, devices and groups - When a host clicks on notification icon - Then a side view appears with all the notifications in it. - -Scenario: Identifying new notifications - When a host wants to know if he got a notification or not, just simply by looking at the number near the symbol tells how many notifications are present - Then if any notification is present then host will open else not. - -Scenario: View upcoming events - Given there is a upcoming event - When user views notifications - Then they should see notifications of upcoming events. - -Scenario: Clicking links inside notifications - Given user has notifications of upcoming events - When they click on the link in that notification - Then they land to that upcoming event page. - -Scenario: No notifications - Given there are no notifications - When a host clikcs on notification symbol, even though they did not get any notification(for the first time) - Then there will be a welcome message. - -Scenario: Notifications in fixometer of activity in talk -#Volunteer engagement. Talk is a very important part of the platform, where people can get involved and be active even if there are no events or groups currently near them. -#In fact, for a number of people it's where they're likely to spend more time. We want to highlight activity and encourage participation and use of Talk as much as possible. - When a user is interested in a topic or few on discourse and something has happened on those topics - Then the user will get a notification about it - And can navigate to talk by clicking the link from the notification. - -Scenario: Notifications of discourse activity in fixometer -#Volunteer engagement. - When a user is related to any groups or events and something has happened on those - Then the user will get a notification about it on talk - And can navigate to fixometer by the notification displayed in the dicourse - And can have a detailed notification about it in fixometer and navigate from there. diff --git a/features/09-Notifications-and-emails/images/email__new-group-near-you.png b/features/09-Notifications-and-emails/images/email__new-group-near-you.png deleted file mode 100644 index 6f2a90c6fc..0000000000 Binary files a/features/09-Notifications-and-emails/images/email__new-group-near-you.png and /dev/null differ diff --git a/features/11-Navigation/AdminMenu.feature b/features/11-Navigation/AdminMenu.feature deleted file mode 100644 index 9f7992b3cc..0000000000 --- a/features/11-Navigation/AdminMenu.feature +++ /dev/null @@ -1,50 +0,0 @@ -Feature: View Menus as an Admin - As an admin - In order to view all the menus - I should be able to click on the menus on dashboard. - -Background: - Given the following account have been created as an admin - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario Outline: Our group and Other menus with parameters - When a host clicks on in the menu - Then they land on page. - - Examples: - | menuitem | landingpage | - | Discussion | https://talk.restarters.net/ | - | Restart Wiki | https://therestartproject.org/wiki/Main_Page | - | The Repair Directory | https://therestartproject.org/repairdirectory/ | - | The Restart Project | https://therestartproject.org/ | - | Help | Help | - | Welcome | Welcome | - -Scenario Outline: Administrator, Reporting and General menus with parameters - When a host clicks on in the menu - Then they land on page. - - Examples: - | menuitem | landingpage | - | Brands | Brands | - | Skills | Skills | - | Group tags | Group tags | - | Categories | Categories | - | Users | Users | - | Roles | Roles | - | Time reporting | Time reporting | - | Event reporting | Event reporting | - | Your profile | Your profile | - | Changed pasword | Changed pasword | - | Logout | Logout | - -Scenario Outline: Events, Devices and Groups menus - When a host clicks on in the menu - Then they land on page. - - Examples: - | menuitem | landingpage | - | Events | Events | - | Devices | Devices | - | Groups | Groups | \ No newline at end of file diff --git a/features/11-Navigation/HostMenu.feature b/features/11-Navigation/HostMenu.feature deleted file mode 100644 index 620328036c..0000000000 --- a/features/11-Navigation/HostMenu.feature +++ /dev/null @@ -1,45 +0,0 @@ -Feature: View Menus as Host - As a Host - In order to view all the menus - I should be able to click on the menus on dashboard. - -Background: - Given the following account have been created as a host - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario Outline: Our group and Other menus - When a host clicks on in the menu - Then they land on page. - - Examples: - | menuitem | landingpage | - | Fixometer | Fixometer | - | Community | Community | - | Restart Wiki | Restart Wiki | - | The Repair Directory | The Repair Directory | - | The Restart Project | The Restart Project | - | Help | Help | - | Welcome | Welcome | - -Scenario Outline: Reporting and General menus - When a host clicks on in the menu - Then they land on page. - - Examples: - | menuitem | landingpage | - | Time reporting | Time reporting | - | Events filter | Events filter | - | Your profile | Your profile | - | Changed pasword | Changed pasword | - | Logout | Logout | - -Scenario Outline: Events, Devices and Groups menus - When a host clicks on in the menu - Then they land on page. - - Examples: - | menuitem | landingpage | - | Events | Events | - | Devices | Devices | - | Groups | Groups | \ No newline at end of file diff --git a/features/11-Navigation/RestarterMenu.feature b/features/11-Navigation/RestarterMenu.feature deleted file mode 100644 index c73fa38454..0000000000 --- a/features/11-Navigation/RestarterMenu.feature +++ /dev/null @@ -1,43 +0,0 @@ -Feature: View Menus as Restarter - As a Restarter - In order to view all the menus - I should be able to click on the menus on dashboard. - -Background: - Given the following account have been created as a restarter - | Email | Password | - | dean@wecreatedigital.co.uk | dean | - -Scenario Outline: Our group and Other menus - When a restarter clicks on in the menu - Then they land on page. - - Examples: - | menuitem | landingpage | - | Fixometer | Fixometer | - | Community | Community | - | Restart Wiki | Restart Wiki | - | The Repair Directory | The Repair Directory | - | The Restart Project | The Restart Project | - | Help | Help | - | Welcome | Welcome | - -Scenario Outline: General menus - When a restarter clicks on in the menu - Then they land on page. - - Examples: - | menuitem | landingpage | - | Your profile | Profile | - | Changed pasword | Changed pasword | - | Logout | Logout | - -Scenario Outline: Events, Devices and Groups menus - When a restarter clicks on in the menu - Then they land on page. - - Examples: - | menuitem | landingpage | - | Events | Events | - | Devices | Devices | - | Groups | Groups | \ No newline at end of file diff --git a/features/12-Wordpress-Push/NewGroups.feature b/features/12-Wordpress-Push/NewGroups.feature deleted file mode 100644 index 9511625c0d..0000000000 --- a/features/12-Wordpress-Push/NewGroups.feature +++ /dev/null @@ -1,19 +0,0 @@ -Feature: New Groups - As an Admin - In order to view the new groups on the wordpress site(main website - https://therestartproject.org/) - I should be able to do it through API call. - -Scenario: View created groups - When an admin approves a group - Then they would see the approved group in the list of groups on the wordpress site - And a group page is created on the wordpress site. - -Scenario: View edited upcoming group - When an admin/host edits an approved group - Then they would see the edited group in the list of groups on the wordpress site - And the changes made would appear on the group page created in wordpress site. - -Scenario: Delete groups - When an admin/host deletes an approved group - Then they will not see the deleted group in the list of groups on the wordpress site - And the group page created in wordpress site will also be deleted. \ No newline at end of file diff --git a/features/12-Wordpress-Push/Upcoming Parties.feature b/features/12-Wordpress-Push/Upcoming Parties.feature deleted file mode 100644 index 0c2e9c1a99..0000000000 --- a/features/12-Wordpress-Push/Upcoming Parties.feature +++ /dev/null @@ -1,30 +0,0 @@ -Feature: Upcoming Parties - -A list of upcoming parties. - -They should display until the end time of the party. - -How many should be listed - all future ones? ------------------------------------------------------------------------------------------------------------------- - -Feature: Upcoming Parties - As an Admin - In order to view the upcoming parties on the wordpress site(main website - https://therestartproject.org/) - I should be able to do it through API call. - -Scenario: View created Upcoming events - When an admin approves an event - Then they would see the approved event in the list of events in upcoming events section on the wordpress site - And an event page is created on the wordpress site - And the event would appear till the end of the event date and time. - -Scenario: View edited upcoming events - When an admin/host edits an approved event - Then they would see the edited event in the list of events in upcoming events section on the wordpress site - And the changes made would appear on the event page created in wordpress site - And the event would appear till the end of the event date and time. - -Scenario: Delete upcoming events - When an admin/host deletes an approved event - Then they will not see the deleted event in the list of events in upcoming events section on the wordpress site - And the event page created in wordpress site will also be deleted. \ No newline at end of file diff --git a/features/13-Wiki/EditingWikiPage.feature b/features/13-Wiki/EditingWikiPage.feature deleted file mode 100644 index bd065fd5dc..0000000000 --- a/features/13-Wiki/EditingWikiPage.feature +++ /dev/null @@ -1,14 +0,0 @@ -Feature: Editing a wiki page - As a user - In order to edit a wiki page - The user should have the wiki badge - -Background: - Given the following user accounts have been created - | Email | Password | - | fry@planetexpress.com | fry! | - -Scenario: Editing wiki page - Given the user has an account in restarters.net and a wiki badge on discourse - When a user logs in to thier wiki account - Then they should have the permission to edit the wiki page. diff --git a/features/API/EventsApi.feature b/features/API/EventsApi.feature deleted file mode 100644 index a87e978db0..0000000000 --- a/features/API/EventsApi.feature +++ /dev/null @@ -1,34 +0,0 @@ -Feature: Events API - -As a repair network -We want to access data about our events -So that we can display it on our own website - -Specifially to being this has been gone for Repair Together. - -The event feed should includes: - -- event id and event name -- event location and geoordinates -- event description -- details about the group the event was organised by -- the event date, start time and end time -- event impact - -Scenario: Event details are returned - Given an event exists for my network - When I access the events feed - Then I see the event details - -Scenario: Event update date is included - Given an event exists for my network - And an amendment is made to that event - When I access the events feed - Then I see the updated date - -Scenario: Filtering by dates - -Scenario: Deleted events are not included - Given an event has been deleted - When I access the events feed - Then I do not see the deleted event diff --git a/features/API/GroupsApi.feature b/features/API/GroupsApi.feature deleted file mode 100644 index 57679db5c1..0000000000 --- a/features/API/GroupsApi.feature +++ /dev/null @@ -1,27 +0,0 @@ -Feature: Groups API - -As a repair network -We want to access data about our groups -So that we can display it on our own website - -Specifially to being this has been gone for Repair Together. - -The groups feed should includes a list of groups for the network: - -- group id and name -- group location (value, country, geocoordinaates) -- website -- description -- image url -- list of upcoming parties -- list of past parties - -Scenario: Group details are returned - Given an event exists for my network - When I access the events feed - Then I see the event details - -Scenario: Deleted events are not included - Given an event for a group has been deleted - When I access the groups feed - Then I do not see the event listed with that group diff --git a/features/API/NetworksApi.feature b/features/API/NetworksApi.feature deleted file mode 100644 index b1f4e58cf5..0000000000 --- a/features/API/NetworksApi.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Networks API - -Get stats about a particular network via the API. - -Scenario: Get stats about network - Given I have permissions on the Restarters network - When I call the API for the Restarters network - Then I get the following stats for the Restarters network diff --git a/features/Networks/AssociateGroupToNetwork.feature b/features/Networks/AssociateGroupToNetwork.feature deleted file mode 100644 index 2ff2038e35..0000000000 --- a/features/Networks/AssociateGroupToNetwork.feature +++ /dev/null @@ -1,21 +0,0 @@ -Feature: Associate Group to Network - -As an Admin -I want to be able to associate existing groups to a network -So that I can organise existing groups into networks - -This is only for Admins. - -Network coordinators can't do this (only invite a group to join a network). - -Being able to add any group to a network (and therefore get moderation rights and additional permissions over that group) would give network coordinators a lot of power. - -Scenario: Associating when editing a group - Given I am an Admin - When I am editing a group - Then I can amend the repair networks(s) to which this group is associated - -Scenario: Associating from network page - Given I am an Admin - When I am viewing a network page - Then there is an option to add a group to this network diff --git a/features/Networks/BulkImport/AddImportedUsersToNetworkDiscussionGroup.feature b/features/Networks/BulkImport/AddImportedUsersToNetworkDiscussionGroup.feature deleted file mode 100644 index 37c16c8fb7..0000000000 --- a/features/Networks/BulkImport/AddImportedUsersToNetworkDiscussionGroup.feature +++ /dev/null @@ -1,34 +0,0 @@ -Feature: Add Imported Users to Network Discussion Group - -As a new network being imported, -We'd like all of our imported users to be added to our discussion group -SO THAT (from network perspective)... we can communicate with all of our members easily via Talk -SO THAT (from Restart perspective)… we have more users engaging in discussion on Talk. - -Each network has its own corresponding network discussion group in Talk (Discourse). - -When we do a bulk import of groups and users, we want to add those new users to the discussion group for the network. - -# Background - -We've done an import for Repair Together only so far. - -# Considerations - -A decision needs to be made by the network as to when we add all of the imported hosts to the discussion group. - -It would be safer not to do it before the network has onboarded them, or at least let them all know it is going to happen, in case they started to receive emails from Discourse before they knew what it was. - - -Scenario: a random selection of 3 users that were imported from the network, are present in the discussion group - - Given the sync of users to Discourse has been run - When I view the list of users in the discussion group admin section - Then for 3 random users associated with the network in Laravel, I can see them in the Discourse discussion group - - -Scenario: the count of users in the network in Laravel matches the count of users in the discussion group - Given the sync of users to Discourse has been run - When I view the list of users in the discussion group admin section - Then the count of users matches the count of users in the network in Laravel (give or take a few existing users, network coordinators etc) - diff --git a/features/Networks/MessageUsers/AddUsersToNetworkDiscussion.feature b/features/Networks/MessageUsers/AddUsersToNetworkDiscussion.feature deleted file mode 100644 index 60b9291c10..0000000000 --- a/features/Networks/MessageUsers/AddUsersToNetworkDiscussion.feature +++ /dev/null @@ -1,23 +0,0 @@ -Feature: Add Users To Network Discussion Group - -As a network coordinator -I want group members automatically added to our discussion group -SO THAT we are able to easily facilitate discussions between all of the users in our network. - -Scenario: User joins group in network - Given I am a network coordinator - And there is a discussion group in place in Talk for my network - When a Restarter follows a repair group in my network - Then the Restarter is also added to my network discussion group in Talk - -Scenario: User joins groups in multiple networks - Given I am a Restarter - And I am already a member of discussion groups on Talk - When I join a repair group in a particular repair network - Then I am added to the repair network discussion group - And I am still a member of the other discussion groups - -Scenario: User joins group not in any specific network - Given the group is in only the generic Restarters network - When someone joins the group - Then they are not added to any new specific network discussion group diff --git a/features/Users/EditLanguageSetting.feature b/features/Users/EditLanguageSetting.feature deleted file mode 100644 index dbea2d98b4..0000000000 --- a/features/Users/EditLanguageSetting.feature +++ /dev/null @@ -1,5 +0,0 @@ -Feature: Edit language settings - -As a user, -So that I have a good experience using the site, -I would like to be able to choose my preferred language in my settings. diff --git a/features/Users/EditProfile.feature b/features/Users/EditProfile.feature deleted file mode 100644 index 0c07c38937..0000000000 --- a/features/Users/EditProfile.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: Edit profile - -As a user of Restarters.net -I want to be able to edit my profile -So that I can keep my details up to date - -Scenario: Editing email - Given I have an account on Discourse - When I update my email address in my profile - Then the email address update is also synced to Discourse diff --git a/features/Users/SyncLanguageSettings.feature b/features/Users/SyncLanguageSettings.feature deleted file mode 100644 index 43da078dea..0000000000 --- a/features/Users/SyncLanguageSettings.feature +++ /dev/null @@ -1,5 +0,0 @@ -Feature: Sync language settings - -As a user, -So that I have a good experience using the site, -I would like my choice of language setting to sync to Talk. diff --git a/specs-site/.gitignore b/specs-site/.gitignore new file mode 100644 index 0000000000..58ed2f44dc --- /dev/null +++ b/specs-site/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.vitepress/cache/ +.vitepress/dist/ +# Generated pages (rebuilt from manifest) +features/ +personas/ +index.md diff --git a/specs-site/.vitepress/config.mts b/specs-site/.vitepress/config.mts new file mode 100644 index 0000000000..fa97a7053b --- /dev/null +++ b/specs-site/.vitepress/config.mts @@ -0,0 +1,53 @@ +import { defineConfig } from 'vitepress' +import fs from 'fs' +import path from 'path' + +function loadManifest() { + const manifestPath = path.resolve(__dirname, '../../docs/specs/manifest.json') + if (!fs.existsSync(manifestPath)) { + return { features: {}, personas: {}, coverage: { annotatedStories: 0, storiesWithTests: 0 } } + } + return JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) +} + +function buildSidebar() { + const manifest = loadManifest() + const featureItems = Object.keys(manifest.features).sort().map(name => ({ + text: `${name} (${manifest.features[name].storyCount})`, + link: `/features/${name.toLowerCase().replace(/\s+/g, '-')}` + })) + const personaItems = Object.keys(manifest.personas).sort().map(name => ({ + text: `${name} (${manifest.personas[name].storyCount})`, + link: `/personas/${name.toLowerCase().replace(/\s+/g, '-')}` + })) + + return [ + { + text: 'Features', + items: featureItems + }, + { + text: 'Personas', + items: personaItems + } + ] +} + +export default defineConfig({ + title: 'Restarters Specifications', + description: 'Living documentation of what Restarters.net does, organised by feature and persona', + base: '/restarters.net/', + themeConfig: { + sidebar: buildSidebar(), + nav: [ + { text: 'Features', link: '/features/' }, + { text: 'Personas', link: '/personas/' } + ], + socialLinks: [ + { icon: 'github', link: 'https://github.com/TheRestartProject/restarters.net' } + ], + search: { + provider: 'local' + } + } +}) diff --git a/specs-site/generate-pages.mjs b/specs-site/generate-pages.mjs new file mode 100644 index 0000000000..f4cc88e4d3 --- /dev/null +++ b/specs-site/generate-pages.mjs @@ -0,0 +1,272 @@ +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const manifestPath = path.resolve(__dirname, '../docs/specs/manifest.json') +const narrativesDir = path.resolve(__dirname, '../docs/specs/narratives') +const featuresDir = path.resolve(__dirname, 'features') +const personasDir = path.resolve(__dirname, 'personas') + +const GITHUB_BASE = 'https://github.com/TheRestartProject/restarters.net/blob/develop' + +function loadManifest() { + if (!fs.existsSync(manifestPath)) { + console.error('No manifest found at docs/specs/manifest.json. Run php artisan specs:extract first.') + process.exit(1) + } + return JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) +} + +function loadNarrative(featureName) { + const slug = featureName.toLowerCase().replace(/\s+/g, '-') + const filePath = path.join(narrativesDir, `${slug}.md`) + if (!fs.existsSync(filePath)) { + return null + } + let content = fs.readFileSync(filePath, 'utf-8') + // Strip the specs:hash comment + content = content.replace(/\n?/, '') + // Strip the top-level heading (we generate our own) + content = content.replace(/^#\s+.+\n+/, '') + return content.trim() +} + +function coverageIndicator(tests) { + if (!tests || tests.length === 0) return ':x: Uncovered' + const hasPhp = tests.some(t => t.file.endsWith('.php')) + const hasJs = tests.some(t => t.file.endsWith('.ts') || t.file.endsWith('.js')) + if (hasPhp && hasJs) return ':white_check_mark: Multi-layer' + return ':white_check_mark: Covered' +} + +function coveragePercent(stories) { + if (stories.length === 0) return '0%' + const covered = stories.filter(s => s.tests && s.tests.length > 0).length + return `${Math.round((covered / stories.length) * 100)}%` +} + +function ensureDir(dir) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } +} + +function generateFeaturePages(manifest) { + ensureDir(featuresDir) + + // Index page + const featureNames = Object.keys(manifest.features).sort() + let indexContent = `# Features\n\nRestarters.net functionality organised by feature area.\n\n` + indexContent += `| Feature | Stories | Personas | Coverage |\n|---------|---------|----------|----------|\n` + + for (const name of featureNames) { + const f = manifest.features[name] + const slug = name.toLowerCase().replace(/\s+/g, '-') + const covered = f.stories.filter(s => s.tests && s.tests.length > 0).length + indexContent += `| [${name}](/features/${slug}) | ${f.storyCount} | ${f.personas.join(', ')} | ${covered}/${f.storyCount} (${coveragePercent(f.stories)}) |\n` + } + + fs.writeFileSync(path.join(featuresDir, 'index.md'), indexContent) + + // Individual feature pages + for (const name of featureNames) { + const f = manifest.features[name] + const slug = name.toLowerCase().replace(/\s+/g, '-') + const narrative = loadNarrative(name) + + let content = `# ${name}\n\n` + + if (f.description) { + content += `> ${f.description}\n\n` + } + + const covered = f.stories.filter(s => s.tests && s.tests.length > 0).length + content += `**${f.storyCount} stories** across ${f.personas.length} personas | **Coverage:** ${covered}/${f.storyCount} (${coveragePercent(f.stories)})\n\n` + + if (narrative) { + content += `## Overview\n\n${narrative}\n\n` + } + + // Group stories by theme, then by persona within each theme + const byTheme = {} + for (const story of f.stories) { + const theme = story.theme || 'General' + if (!byTheme[theme]) byTheme[theme] = [] + byTheme[theme].push(story) + } + + const themeNames = Object.keys(byTheme).sort((a, b) => { + if (a === 'General') return 1 + if (b === 'General') return -1 + return a.localeCompare(b) + }) + + for (const theme of themeNames) { + const stories = byTheme[theme] + content += `## ${theme}\n\n` + content += `| Persona | Story | Method | Tests |\n|---------|-------|--------|-------|\n` + + stories.sort((a, b) => a.persona.localeCompare(b.persona) || a.method.localeCompare(b.method)) + + for (const story of stories) { + const methodLink = `[\`${story.method}\`](${GITHUB_BASE}/${story.file})` + content += `| **${story.persona}** | ${story.story} | ${methodLink} | ${coverageIndicator(story.tests)} |\n` + } + + content += `\n` + } + + // Sources + content += `## Source Files\n\n` + for (const source of f.sources.sort()) { + content += `- [\`${source}\`](${GITHUB_BASE}/${source})\n` + } + + fs.writeFileSync(path.join(featuresDir, `${slug}.md`), content) + } +} + +function generatePersonaPages(manifest) { + ensureDir(personasDir) + + // Index page + const personaNames = Object.keys(manifest.personas).sort() + let indexContent = `# Personas\n\nRestarters.net functionality organised by user persona.\n\n` + indexContent += `| Persona | Features | Stories | Coverage |\n|---------|----------|---------|----------|\n` + + for (const name of personaNames) { + const p = manifest.personas[name] + const slug = name.toLowerCase().replace(/\s+/g, '-') + + // Calculate coverage for this persona across all features + let totalStories = 0 + let coveredStories = 0 + for (const featureName of p.features) { + const feature = manifest.features[featureName] + if (!feature) continue + for (const story of feature.stories) { + if (story.persona === name) { + totalStories++ + if (story.tests && story.tests.length > 0) coveredStories++ + } + } + } + + indexContent += `| [${name}](/personas/${slug}) | ${p.features.length} | ${p.storyCount} | ${coveredStories}/${totalStories} (${totalStories > 0 ? Math.round((coveredStories / totalStories) * 100) : 0}%) |\n` + } + + fs.writeFileSync(path.join(personasDir, 'index.md'), indexContent) + + // Individual persona pages + for (const name of personaNames) { + const p = manifest.personas[name] + const slug = name.toLowerCase().replace(/\s+/g, '-') + + let content = `# ${name}\n\n` + content += `**${p.storyCount} stories** across ${p.features.length} features\n\n` + + for (const featureName of p.features.sort()) { + const feature = manifest.features[featureName] + if (!feature) continue + + const personaStories = feature.stories.filter(s => s.persona === name) + if (personaStories.length === 0) continue + + const featureSlug = featureName.toLowerCase().replace(/\s+/g, '-') + content += `## [${featureName}](/features/${featureSlug})\n\n` + + const byTheme = {} + for (const story of personaStories) { + const theme = story.theme || 'General' + if (!byTheme[theme]) byTheme[theme] = [] + byTheme[theme].push(story) + } + + const themeNames = Object.keys(byTheme).sort((a, b) => { + if (a === 'General') return 1 + if (b === 'General') return -1 + return a.localeCompare(b) + }) + + for (const theme of themeNames) { + content += `### ${theme}\n\n` + content += `| Story | Method | Tests |\n|-------|--------|-------|\n` + for (const story of byTheme[theme]) { + const methodLink = `[\`${story.method}\`](${GITHUB_BASE}/${story.file})` + content += `| ${story.story} | ${methodLink} | ${coverageIndicator(story.tests)} |\n` + } + content += `\n` + } + } + + fs.writeFileSync(path.join(personasDir, `${slug}.md`), content) + } +} + +function generateHomePage(manifest) { + const totalStories = manifest.coverage.annotatedStories + const withTests = manifest.coverage.storiesWithTests + const featureCount = Object.keys(manifest.features).length + const personaCount = Object.keys(manifest.personas).length + + let content = `--- +layout: home +hero: + name: Restarters Specifications + tagline: Living documentation of what Restarters.net does + actions: + - theme: brand + text: Browse by Feature + link: /features/ + - theme: alt + text: Browse by Persona + link: /personas/ +--- + +## About Restarters.net + +Restarters.net is the platform behind the global community repair movement. It brings together volunteer fixers, event hosts, and repair networks to organise community repair events and measure their environmental impact. + +The platform combines three core modules: **The Fixometer** for organising repair events and recording their impact, **Restarters Talk** for community discussion, and **Restarters Wiki** for collectively produced repair knowledge. Groups around the world use it to run events, log device repairs, track waste prevented, and coordinate through regional networks. + +This site is the living specification — every user story listed here is extracted directly from the codebase and linked to its test coverage. It updates automatically as the code changes. + +## At a Glance + +| | | +|---|---| +| **Features** | ${featureCount} | +| **Personas** | ${personaCount} | +| **User Stories** | ${totalStories} | +| **Stories with Tests** | ${withTests} (${totalStories > 0 ? Math.round((withTests / totalStories) * 100) : 0}%) | +| **Generated** | ${manifest.generatedAt || 'Unknown'} | + +## Features + +` + + for (const name of Object.keys(manifest.features).sort()) { + const f = manifest.features[name] + const slug = name.toLowerCase().replace(/\s+/g, '-') + content += `- [**${name}**](/features/${slug}) — ${f.description || `${f.storyCount} stories`} (${f.storyCount} stories)\n` + } + + content += `\n## Personas\n\n` + + for (const name of Object.keys(manifest.personas).sort()) { + const p = manifest.personas[name] + const slug = name.toLowerCase().replace(/\s+/g, '-') + content += `- [**${name}**](/personas/${slug}) — ${p.storyCount} stories across ${p.features.join(', ')}\n` + } + + fs.writeFileSync(path.join(__dirname, 'index.md'), content) +} + +// Main +const manifest = loadManifest() +console.log(`Generating pages from manifest (${manifest.coverage.annotatedStories} stories, ${Object.keys(manifest.features).length} features, ${Object.keys(manifest.personas).length} personas)`) +generateHomePage(manifest) +generateFeaturePages(manifest) +generatePersonaPages(manifest) +console.log('Pages generated successfully') diff --git a/specs-site/package.json b/specs-site/package.json new file mode 100644 index 0000000000..9d57fc0ab8 --- /dev/null +++ b/specs-site/package.json @@ -0,0 +1,13 @@ +{ + "name": "restarters-specs", + "private": true, + "scripts": { + "prebuild": "node generate-pages.mjs", + "dev": "npm run prebuild && vitepress dev", + "build": "npm run prebuild && vitepress build", + "preview": "vitepress preview" + }, + "devDependencies": { + "vitepress": "^1.6.0" + } +} diff --git a/tests/Feature/Admin/Users/ViewUsersTest.php b/tests/Feature/Admin/Users/ViewUsersTest.php index 1647eb214a..fe44d49ea9 100644 --- a/tests/Feature/Admin/Users/ViewUsersTest.php +++ b/tests/Feature/Admin/Users/ViewUsersTest.php @@ -22,7 +22,10 @@ protected function setUp(): void $this->actingAs($admin); } - /** @test */ + /** + * @test + * @story:UserController::all + */ public function an_admin_can_view_list_of_users() { // Given we have users in the database @@ -35,7 +38,10 @@ public function an_admin_can_view_list_of_users() $response->assertSeeText($users[0]->name); } - /** @test */ + /** + * @test + * @story:UserController::all + */ public function an_admin_can_see_how_many_total_users_in_the_list() { // Given we have users in the database @@ -48,7 +54,10 @@ public function an_admin_can_see_how_many_total_users_in_the_list() $response->assertSeeText(42); } - /** @test */ + /** + * @test + * @story:UserController::all + */ public function admin_can_see_users_last_login_time() { // Given we have a user who has just logged in @@ -64,7 +73,10 @@ public function admin_can_see_users_last_login_time() $response->assertSeeText($lastLogin->diffForHumans(null, true)); } - /** @test */ + /** + * @test + * @story:UserController::search + */ public function admin_can_see_users_last_login_time_on_filtered_results() { // Given we have a user who has just logged in @@ -80,7 +92,10 @@ public function admin_can_see_users_last_login_time_on_filtered_results() $response->assertSeeText($lastLogin->diffForHumans(null, true)); } - /** @test */ + /** + * @test + * @story:UserController::search + */ public function admin_can_sort_user_list_by_last_login() { // Given we have users with various login times diff --git a/tests/Feature/Admin/Users/WikiLoginTests.php b/tests/Feature/Admin/Users/WikiLoginTests.php index a0ba989370..8d18412845 100644 --- a/tests/Feature/Admin/Users/WikiLoginTests.php +++ b/tests/Feature/Admin/Users/WikiLoginTests.php @@ -27,7 +27,10 @@ protected function setUp(): void DB::statement('SET foreign_key_checks=1'); } - /** @test */ + /** + * @test + * @story:LoginController::login + */ public function if_flagged_for_creation_create_when_logging_in() { $this->withoutExceptionHandling(); @@ -51,7 +54,10 @@ public function if_flagged_for_creation_create_when_logging_in() $this->assertEquals(WikiSyncStatus::Created, $user->wiki_sync_status); } - /** @test */ + /** + * @test + * @story:LoginController::login + */ public function if_not_flagged_for_creation() { $this->withoutExceptionHandling(); @@ -75,7 +81,10 @@ public function if_not_flagged_for_creation() $this->assertEquals(WikiSyncStatus::DoNotCreate, $user->wiki_sync_status); } - /** @test */ + /** + * @test + * @story:LoginController::login + */ public function if_already_created() { $this->withoutExceptionHandling(); @@ -99,7 +108,10 @@ public function if_already_created() $this->assertEquals(WikiSyncStatus::Created, $user->wiki_sync_status); } - /** @test */ + /** + * @test + * @story:UserController::postProfilePasswordEdit + */ public function if_wiki_user_changes_password() { $this->withoutExceptionHandling(); @@ -120,7 +132,10 @@ public function if_wiki_user_changes_password() // Then the user's wiki password should be changed to match } - /** @test */ + /** + * @test + * @story:LoginController::login + */ public function login_succeeds_when_wiki_unavailable() { $this->withoutExceptionHandling(); diff --git a/tests/Feature/Alerts/AlertsTest.php b/tests/Feature/Alerts/AlertsTest.php index 16a1303cd7..4cff9cd1d7 100644 --- a/tests/Feature/Alerts/AlertsTest.php +++ b/tests/Feature/Alerts/AlertsTest.php @@ -18,6 +18,7 @@ protected function setUp(): void Cache::clear('alerts'); } + /** @story:AlertController::listAlertsv2 */ public function testListNonePresent() { // List - no alerts present. @@ -29,6 +30,9 @@ public function testListNonePresent() } /** + * @story:AlertController::addAlertv2 + * @story:AlertController::listAlertsv2 + * @story:AlertController::updateAlertv2 * @dataProvider roleProvider */ public function testCreate($role, $allowed) { diff --git a/tests/Feature/Brands/BrandsTest.php b/tests/Feature/Brands/BrandsTest.php index 306561f05e..79ae690132 100644 --- a/tests/Feature/Brands/BrandsTest.php +++ b/tests/Feature/Brands/BrandsTest.php @@ -11,6 +11,13 @@ class BrandsTest extends TestCase { + /** + * @story:BrandsController::postCreateBrand + * @story:BrandsController::index + * @story:BrandsController::getEditBrand + * @story:BrandsController::postEditBrand + * @story:BrandsController::getDeleteBrand + */ public function testBasic() { $this->loginAsTestUser(Role::ADMINISTRATOR); @@ -47,6 +54,13 @@ public function testBasic() $response->assertSessionHas('message'); } + /** + * @story:BrandsController::postCreateBrand + * @story:BrandsController::index + * @story:BrandsController::getEditBrand + * @story:BrandsController::postEditBrand + * @story:BrandsController::getDeleteBrand + */ public function testErrors() { $this->loginAsTestUser(Role::RESTARTER); diff --git a/tests/Feature/Calendar/CalendarTest.php b/tests/Feature/Calendar/CalendarTest.php index f158de5007..bff674001b 100644 --- a/tests/Feature/Calendar/CalendarTest.php +++ b/tests/Feature/Calendar/CalendarTest.php @@ -93,6 +93,7 @@ protected function setUp(): void ]); } + /** @story:CalendarEventsController::allEventsByUser */ public function testByUser() { // Valid hash. $response = $this->get('/calendar/user/' . $this->host->calendar_hash); @@ -107,6 +108,7 @@ public function testByUser() { $response = $this->get('/calendar/user/' . $this->host->calendar_hash . '1'); } + /** @story:CalendarEventsController::allEventsByGroup */ public function testByGroup() { // One event. $response = $this->get('/calendar/group/' . $this->group->idgroups); @@ -120,6 +122,7 @@ public function testByGroup() { $response = $this->get('/calendar/group/' . $this->group2->idgroups); } + /** @story:CalendarEventsController::allEventsByNetwork */ public function testByNetwork() { // One event. $response = $this->get('/calendar/network/' . $this->network->id); @@ -134,6 +137,7 @@ public function testByNetwork() { $response = $this->get('/calendar/network/' . $network->id); } + /** @story:CalendarEventsController::allEventsByArea */ public function testByArea() { // One event. $response = $this->get('/calendar/group-area/London'); @@ -159,6 +163,7 @@ public function testAll() { $response = $this->get('/calendar/all-events/' . env('CALENDAR_HASH') . '1'); } + /** @story:CalendarEventsController::allEventsByUser */ public function testCancelled() { $this->event->cancelled = 1; $this->event->save(); @@ -167,6 +172,7 @@ public function testCancelled() { $this->expectOutputRegex('/CANCELLED/'); } + /** @story:CalendarEventsController::allEventsByUser */ public function testEventNotApproved() { $this->event->approved = false; $this->event->save(); @@ -175,6 +181,7 @@ public function testEventNotApproved() { $this->expectOutputRegex('/TENTATIVE/'); } + /** @story:CalendarEventsController::allEventsByUser */ public function testGroupNotApproved() { $this->group->approved = false; $this->group->save(); @@ -183,6 +190,7 @@ public function testGroupNotApproved() { $this->expectOutputRegex('/TENTATIVE/'); } + /** @story:CalendarEventsController::allEventsByUser */ public function testEventNotVisible() { $host = User::factory()->create([ 'latitude' => 50.64, diff --git a/tests/Feature/Category/CategoryTest.php b/tests/Feature/Category/CategoryTest.php index 0239aa66f1..ef3921f821 100644 --- a/tests/Feature/Category/CategoryTest.php +++ b/tests/Feature/Category/CategoryTest.php @@ -10,6 +10,11 @@ class CategoryTest extends TestCase { + /** + * @story:CategoryController::index + * @story:CategoryController::getEditCategory + * @story:CategoryController::postEditCategory + */ public function testBasic() { $this->loginAsTestUser(Role::ADMINISTRATOR); @@ -41,6 +46,10 @@ public function testBasic() $response->assertSessionHas('success'); } + /** + * @story:CategoryController::getEditCategory + * @story:CategoryController::postEditCategory + */ public function testErrors() { $this->loginAsTestUser(Role::RESTARTER); diff --git a/tests/Feature/Dashboard/BasicTest.php b/tests/Feature/Dashboard/BasicTest.php index 78c37399a4..bf8ca0b41b 100644 --- a/tests/Feature/Dashboard/BasicTest.php +++ b/tests/Feature/Dashboard/BasicTest.php @@ -24,7 +24,9 @@ protected function setUp(): void } /** - *@dataProvider provider + * @story:DashboardController::index + * @story:DiscourseController::discussionTopics + * @dataProvider provider */ public function testPageLoads($city, $country, $lat, $lng, $nearbyGroupCount) { @@ -84,6 +86,11 @@ public function provider() ]; } + /** + * @story:DashboardController::index + * @story:PartyController::index + * @story:EventController::updateEventv2 + */ public function testUpcomingEvents() { $host = User::factory()->restarter()->create(); diff --git a/tests/Feature/Dashboard/LanguageSwitcherTest.php b/tests/Feature/Dashboard/LanguageSwitcherTest.php index 53d4b6f2eb..721beea08b 100644 --- a/tests/Feature/Dashboard/LanguageSwitcherTest.php +++ b/tests/Feature/Dashboard/LanguageSwitcherTest.php @@ -13,6 +13,7 @@ class LanguageSwitcherTest extends TestCase { + /** @story:LocaleController::setLang */ public function testSwitchEndpoint() { $this->loginAsTestUser(Role::ADMINISTRATOR); diff --git a/tests/Feature/Devices/APIv2DeviceTest.php b/tests/Feature/Devices/APIv2DeviceTest.php index 571c69bb70..9fe337d786 100644 --- a/tests/Feature/Devices/APIv2DeviceTest.php +++ b/tests/Feature/Devices/APIv2DeviceTest.php @@ -28,6 +28,7 @@ class APIv2DeviceTest extends TestCase * This logic duplicates that in DeviceController, but it's worth testing to make sure that the API is * behaving as we'd expect from the DB entries. * + * @story:DeviceController::getDevicev2 * @dataProvider providerDevice */ public function testGetDevice($repair_status_str, $parts_provider_str, $next_steps_str, $barrierstr) { @@ -149,6 +150,8 @@ public function testGetDevice($repair_status_str, $parts_provider_str, $next_ste /** * Create a device over the API and check it retrieves as expected. * + * @story:DeviceController::createDevicev2 + * @story:DeviceController::getDevicev2 * @dataProvider providerDevice */ public function testCreate($repair_status_str, $parts_provider_str, $next_steps_str, $barrierstr) { diff --git a/tests/Feature/Devices/CategoryTest.php b/tests/Feature/Devices/CategoryTest.php index 1964092c11..1971d2a866 100644 --- a/tests/Feature/Devices/CategoryTest.php +++ b/tests/Feature/Devices/CategoryTest.php @@ -11,6 +11,10 @@ class CategoryTest extends TestCase { + /** + * @story:DeviceController::createDevicev2 + * @story:DeviceController::updateDevicev2 + */ public function testCategoryChange() { $event = Party::factory()->create(); @@ -50,6 +54,7 @@ public function testCategoryChange() self::assertEquals($device->category, 46); } + /** @story:ItemController::listItemsv2 */ public function testListItems() { $cat1 = Category::factory()->create([ 'idcategories' => 444, diff --git a/tests/Feature/Devices/EditTest.php b/tests/Feature/Devices/EditTest.php index be3d7b11fb..2a5071e624 100644 --- a/tests/Feature/Devices/EditTest.php +++ b/tests/Feature/Devices/EditTest.php @@ -30,6 +30,11 @@ protected function setUp(): void $this->withoutExceptionHandling(); } + /** + * @story:DeviceController::createDevicev2 + * @story:DeviceController::updateDevicev2 + * @story:DeviceController::deleteDevicev2 + */ public function testEdit() { $iddevices = $this->createDevice($this->event->idevents, 'misc'); @@ -56,6 +61,10 @@ public function testEdit() $this->deleteDevice($iddevices); } + /** + * @story:DeviceController::createDevicev2 + * @story:DeviceController::updateDevicev2 + */ public function testEditAsNetworkCoordinator() { $network = Network::factory()->create(); @@ -83,6 +92,10 @@ public function testEditAsNetworkCoordinator() $response->assertSuccessful(); } + /** + * @story:DeviceController::imageUpload + * @story:DeviceController::deleteImage + */ public function testDeviceEditAddImage() { Storage::fake('avatars'); $user = User::factory()->administrator()->create(); @@ -146,6 +159,10 @@ public function testDeviceEditAddImage() { $this->assertEquals('Thank you, the image has been deleted', \Session::get('message')); } + /** + * @story:DeviceController::imageUpload + * @story:DeviceController::deleteImage + */ public function testDeviceAddAddImage() { Storage::fake('avatars'); $user = User::factory()->administrator()->create(); @@ -202,6 +219,7 @@ public function testDeviceAddAddImage() { $this->assertEquals('Thank you, the image has been deleted', \Session::get('message')); } + /** @story:DeviceController::updateDevicev2 */ public function testNextSteps() { $iddevices = $this->createDevice($this->event->idevents, 'misc'); diff --git a/tests/Feature/Devices/NullAgeProblemTest.php b/tests/Feature/Devices/NullAgeProblemTest.php index a3ad994c0b..0942f78b0f 100644 --- a/tests/Feature/Devices/NullAgeProblemTest.php +++ b/tests/Feature/Devices/NullAgeProblemTest.php @@ -11,6 +11,7 @@ class NullAgeProblemTest extends TestCase { + /** @story:DeviceController::createDevicev2 */ public function testNullAgeCreate() { $event = Party::factory()->create(); @@ -24,6 +25,7 @@ public function testNullAgeCreate() $iddevices = $this->createDevice($event->idevents, 'misc', null, null); } + /** @story:DeviceController::updateDevicev2 */ public function testNullAgeEdit() { $event = Party::factory()->create(); diff --git a/tests/Feature/Devices/NullEstimateProblemTest.php b/tests/Feature/Devices/NullEstimateProblemTest.php index 620941c1ac..54d0f5fade 100644 --- a/tests/Feature/Devices/NullEstimateProblemTest.php +++ b/tests/Feature/Devices/NullEstimateProblemTest.php @@ -11,6 +11,7 @@ class NullEstimateProblemTest extends TestCase { + /** @story:DeviceController::createDevicev2 */ public function testNullEstimateCreate() { $event = Party::factory()->create(); @@ -23,6 +24,7 @@ public function testNullEstimateCreate() $iddevices = $this->createDevice($event->idevents, 'misc', null, 1, null); } + /** @story:DeviceController::updateDevicev2 */ public function testNullEstimateEdit() { $event = Party::factory()->create(); diff --git a/tests/Feature/Devices/NullProblemTest.php b/tests/Feature/Devices/NullProblemTest.php index ebc48636a9..1128e9e098 100644 --- a/tests/Feature/Devices/NullProblemTest.php +++ b/tests/Feature/Devices/NullProblemTest.php @@ -26,7 +26,10 @@ protected function setUp(): void $this->withoutExceptionHandling(); } - /** @test */ + /** + * @test + * @story:DeviceController::createDevicev2 + */ public function null_problem_mapped_to_empty_string() { $iddevices = $this->createDevice($this->event->idevents, 'misc', null, 1, 100, null); diff --git a/tests/Feature/Devices/SparePartsTest.php b/tests/Feature/Devices/SparePartsTest.php index 3f97328b15..ead6127012 100644 --- a/tests/Feature/Devices/SparePartsTest.php +++ b/tests/Feature/Devices/SparePartsTest.php @@ -31,7 +31,10 @@ protected function setUp(): void $this->withoutExceptionHandling(); } - /** @test */ + /** + * @test + * @story:DeviceController::createDevicev2 + */ public function recording_spare_parts_from_manufacturer() { $iddevices = $this->createDevice($this->event->idevents, @@ -47,7 +50,10 @@ public function recording_spare_parts_from_manufacturer() $this->assertEquals(trans('partials.fixed'), $device->getRepairStatus()); } - /** @test */ + /** + * @test + * @story:DeviceController::createDevicev2 + */ public function recording_spare_parts_from_third_party() { $this->device_inputs['repair_status'] = Device::REPAIR_STATUS_REPAIRABLE; @@ -66,7 +72,10 @@ public function recording_spare_parts_from_third_party() $this->assertEquals(trans('partials.yes_third_party'), $device->getSpareParts()); } - /** @test */ + /** + * @test + * @story:DeviceController::createDevicev2 + */ public function recording_no_spare_parts_needed() { $iddevices = $this->createDevice($this->event->idevents, @@ -81,7 +90,10 @@ public function recording_no_spare_parts_needed() $this->assertEquals(trans('partials.no'), $device->getSpareParts()); } - /** @test */ + /** + * @test + * @story:DeviceController::createDevicev2 + */ public function recording_spare_parts_related_barrier() { $iddevices = $this->createDevice($this->event->idevents, @@ -95,7 +107,10 @@ public function recording_spare_parts_related_barrier() } - /** @test */ + /** + * @test + * @story:DeviceController::createDevicev2 + */ public function recording_no_spare_parts_related_barrier() { $iddevices = $this->createDevice($this->event->idevents, diff --git a/tests/Feature/Devices/TooManyMiscTest.php b/tests/Feature/Devices/TooManyMiscTest.php index 85104bb138..4e85bbf040 100644 --- a/tests/Feature/Devices/TooManyMiscTest.php +++ b/tests/Feature/Devices/TooManyMiscTest.php @@ -13,6 +13,7 @@ class TooManyMiscTest extends TestCase { /** + * @story:DeviceController::createDevicev2 * @dataProvider provider */ public function testTooMany($count, $notif) diff --git a/tests/Feature/Events/APIv2EventTest.php b/tests/Feature/Events/APIv2EventTest.php index f11b3e42d7..85f731f248 100644 --- a/tests/Feature/Events/APIv2EventTest.php +++ b/tests/Feature/Events/APIv2EventTest.php @@ -18,6 +18,11 @@ class APIv2EventTest extends TestCase { + /** + * @story:GroupController::getEventsForGroupv2 + * @story:EventController::getEventv2 + * @story:EventController::moderateEventsv2 + */ public function testGetEventsForGroup() { $user = User::factory()->administrator()->create([ 'api_token' => '1234', @@ -78,6 +83,11 @@ public function testGetEventsForGroup() { self::assertFalse($json[1]['approved']); } + /** + * @story:GroupController::getEventsForGroupv2 + * @story:EventController::getEventv2 + * @story:NetworkController::getNetworkEventsv2 + */ public function testGetEventsForUnapprovedGroup() { $user = User::factory()->administrator()->create([ 'api_token' => '1234', @@ -110,6 +120,10 @@ public function testGetEventsForUnapprovedGroup() { $response->assertSuccessful(); } + /** + * @story:EventController::getEventv2 + * @story:EventController::getEventsByUsersNetworks + */ public function testMaxUpdatedAt() { $user = User::factory()->administrator()->create([ 'api_token' => '1234', @@ -161,6 +175,8 @@ public function testMaxUpdatedAt() { * @param $role * @return void * @dataProvider roleProvider + * @story:GroupController::createGroupv2 + * @story:EventController::createEventv2 */ public function testCreateLoggedOutUsingKey($role) { switch ($role) { @@ -216,6 +232,7 @@ public function roleProvider() { ]; } + /** @story:EventController::updateEventv2 */ public function testEditForbidden() { $user1 = User::factory()->host()->create([ 'api_token' => '1234', @@ -244,6 +261,7 @@ public function testEditForbidden() { $this->patch('/api/v2/events/'.$id1, $this->eventAttributesToAPI($eventData)); } + /** @story:EventController::createEventv2 */ public function testCreateEventGeocodeFailure() { $user = User::factory()->host()->create(); @@ -283,6 +301,7 @@ public function testCreateEventGeocodeFailure() ]); } + /** @story:EventController::createEventv2 */ public function testCreateEventInvalidTimezone() { $user = User::factory()->host()->create(); @@ -322,6 +341,7 @@ public function testCreateEventInvalidTimezone() ]); } + /** @story:EventController::getEventv2 */ public function testEmptyNetworkData() { $user = User::factory()->administrator()->create([ 'api_token' => '1234', @@ -341,6 +361,7 @@ public function testEmptyNetworkData() { assertEquals(null, $json['data']['network_data']); } + /** @story:EventController::updateEventv2 */ public function testNetworkCoordinatorCanApprove() { $network = Network::factory()->create(); $group = Group::factory()->create(); diff --git a/tests/Feature/Events/AddRemoveVolunteerTest.php b/tests/Feature/Events/AddRemoveVolunteerTest.php index db6c9486cb..43a8ff1c58 100644 --- a/tests/Feature/Events/AddRemoveVolunteerTest.php +++ b/tests/Feature/Events/AddRemoveVolunteerTest.php @@ -25,8 +25,12 @@ class AddRemoveVolunteerTest extends TestCase { /** * @dataProvider roleProvider + * @story:EventController::addVolunteer + * @story:EventController::listVolunteers + * @story:PartyController::removeVolunteer + * @story:PartyController::postSendInvite + * @story:PartyController::view */ - public function testAddRemove($role, $addrole, $shouldBeHost) { $this->withoutExceptionHandling(); @@ -215,6 +219,10 @@ public function roleProvider() { ]; } + /** @story:UserGroupsController::leave + * @story:UserController::edit + * @story:UserController::postAdminEdit + */ public function testAdminRemoveReaddHost() { $this->withoutExceptionHandling(); diff --git a/tests/Feature/Events/AttendanceTest.php b/tests/Feature/Events/AttendanceTest.php index c3e1bf8b6c..e55b7c7c8d 100644 --- a/tests/Feature/Events/AttendanceTest.php +++ b/tests/Feature/Events/AttendanceTest.php @@ -37,6 +37,7 @@ protected function setUp(): void $this->party = $this->group->parties()->latest()->first(); } + /** @story:PartyController::updateQuantity */ public function testParticipants() { // Initial count will be 0. self::assertEquals(0, $this->party->pax); @@ -52,6 +53,7 @@ public function testParticipants() { } + /** @story:PartyController::updateVolunteerQuantity */ public function testVolunteers() { // Initial count will be 1, for the host. self::assertEquals(1, $this->party->volunteers); diff --git a/tests/Feature/Events/CreateEventTest.php b/tests/Feature/Events/CreateEventTest.php index 393f7f1809..a87ce58919 100644 --- a/tests/Feature/Events/CreateEventTest.php +++ b/tests/Feature/Events/CreateEventTest.php @@ -56,7 +56,10 @@ protected function setUp(): void }); } - /** @test */ + /** + * @test + * @story:PartyController::create + */ public function a_host_without_a_group_cant_create_an_event() { $this->withoutExceptionHandling(); @@ -71,6 +74,11 @@ public function a_host_without_a_group_cant_create_an_event() /** * @test * @dataProvider roles + * @story:EventController::createEventv2 + * @story:PartyController::create + * @story:PartyController::view + * @story:GroupController::getEventsForGroupv2 + * @story:PartyController::index */ public function a_host_with_a_group_can_create_an_event($data) { @@ -262,7 +270,10 @@ public function roles() ]; } - /** @test */ + /** + * @test + * @story:PartyController::duplicate + */ public function a_host_can_duplicate_an_event() { $this->withoutExceptionHandling(); @@ -301,6 +312,7 @@ public function providerTrueFalse() * @test * * @dataProvider providerTrueFalse + * @story:EventController::createEventv2 */ public function emails_sent_when_created($notify) { @@ -358,7 +370,11 @@ public function emails_sent_when_created($notify) } } - /** @test */ + /** + * @test + * @story:EventController::createEventv2 + * @story:EventController::updateEventv2 + */ public function emails_sent_to_restarters_when_upcoming_event_approved() { $this->withoutExceptionHandling(); @@ -420,7 +436,11 @@ function ($notification, $channels, $user) use ($group, $host) { $this->patch('/api/v2/events/'.$event->idevents, $this->eventAttributesToAPI($eventData)); } - /** @test */ + /** + * @test + * @story:EventController::createEventv2 + * @story:EventController::updateEventv2 + */ public function emails_not_sent_to_volunteers_when_past_event_approved() { $this->withoutExceptionHandling(); @@ -453,7 +473,10 @@ public function emails_not_sent_to_volunteers_when_past_event_approved() ); } - /** @test */ + /** + * @test + * @story:EventController::createEventv2 + */ public function emails_sent_to_coordinators_when_event_created() { $this->withoutExceptionHandling(); @@ -487,7 +510,13 @@ public function emails_sent_to_coordinators_when_event_created() ); } - /** @test */ + /** + * @test + * @story:EventController::createEventv2 + * @story:EventController::addVolunteer + * @story:PartyController::removeVolunteer + * @story:GroupController::getVolunteersForGroupv2 + */ public function a_host_can_be_added_later() { $this->withoutExceptionHandling(); @@ -591,7 +620,8 @@ public function provider() /** * @test - **@dataProvider provider + * @dataProvider provider + * @story:EventController::createEventv2 */ public function an_event_can_be_auto_approved($autoApprove, $approved) { @@ -619,6 +649,7 @@ public function an_event_can_be_auto_approved($autoApprove, $approved) /** * @test + * @story:EventController::createEventv2 */ public function a_past_event_is_not_upcoming() { $host = User::factory()->administrator()->create(); @@ -643,6 +674,7 @@ public function a_past_event_is_not_upcoming() { /** * @test + * @story:EventController::createEventv2 */ public function a_future_event_is_upcoming() { $host = User::factory()->administrator()->create(); @@ -667,6 +699,9 @@ public function a_future_event_is_upcoming() { /** * @test + * @story:GroupController::deleteVolunteerForGroupv2 + * @story:EventController::createEventv2 + * @story:EventController::updateEventv2 */ public function no_notification_after_leaving() { Notification::fake(); @@ -716,6 +751,7 @@ public function no_notification_after_leaving() { * @test * * @dataProvider providerTrueFalse + * @story:EventController::createEventv2 */ public function notifications_are_queued_as_expected($notify) { @@ -772,7 +808,12 @@ public function notifications_are_queued_as_expected($notify) ); } - /** @test */ + /** + * @test + * @story:PartyController::create + * @story:EventController::createEventv2 + * @story:PartyController::edit + */ public function network_coordinator_other_group() { $network = Network::factory()->create(); @@ -817,6 +858,8 @@ public function network_coordinator_other_group() { /** * @dataProvider invalidEmailProvider + * @story:EventController::createEventv2 + * @story:EventController::addVolunteer */ public function an_invalid_email_is_trapped($email, $valid) { diff --git a/tests/Feature/Events/DeleteEventTest.php b/tests/Feature/Events/DeleteEventTest.php index 78bd016d6e..5eb1f91c9a 100644 --- a/tests/Feature/Events/DeleteEventTest.php +++ b/tests/Feature/Events/DeleteEventTest.php @@ -35,7 +35,14 @@ protected function setUp(): void parent::setUp(); } - /** @test */ + /** + * @test + * @story:PartyController::deleteEvent + * @story:OutboundController::info + * @story:PartyController::getJoinEvent + * @story:ApiController::groupStats + * @story:ApiController::partyStats + */ public function an_admin_can_delete_an_event() { $this->withoutExceptionHandling(); @@ -93,6 +100,9 @@ public function an_admin_can_delete_an_event() /** * @test * @dataProvider roleProvider + * @story:PartyController::view + * @story:PartyController::deleteEvent + * @story:PartyController::edit */ public function view_edit_deleted_event($role) { @@ -272,6 +282,7 @@ public function provider() /** * @test * @dataProvider provider + * @story:PartyController::view */ public function candelete_flag($role, $pastFuture, $addDevice, $canDelete) { @@ -317,6 +328,8 @@ public function candelete_flag($role, $pastFuture, $addDevice, $canDelete) /** * @test + * @story:PartyController::getJoinEvent + * @story:PartyController::getContributions */ public function request_review() { diff --git a/tests/Feature/Events/EventRequestReviewEmailTest.php b/tests/Feature/Events/EventRequestReviewEmailTest.php index 8ce0e817c5..ff4cea1c86 100644 --- a/tests/Feature/Events/EventRequestReviewEmailTest.php +++ b/tests/Feature/Events/EventRequestReviewEmailTest.php @@ -39,7 +39,10 @@ protected function setUp(): void parent::setUp(); } - /** @test */ + /** + * @test + * @story:PartyController::getContributions + */ public function a_request_review_email_is_sent_to_volunteer() { Notification::fake(); diff --git a/tests/Feature/Events/ExportTest.php b/tests/Feature/Events/ExportTest.php index 6c783a9ee3..9513505f00 100644 --- a/tests/Feature/Events/ExportTest.php +++ b/tests/Feature/Events/ExportTest.php @@ -18,6 +18,11 @@ class ExportTest extends TestCase { /** * @dataProvider roleProvider + * @story:ExportController::groupEvents + * @story:ExportController::networkEvents + * @story:ExportController::devices + * @story:ExportController::devicesEvent + * @story:ExportController::devicesGroup */ public function testExport($role) { @@ -227,6 +232,10 @@ public function testExport($role) } } + /** + * @story:ExportController::devicesGroup + * @story:ExportController::devicesEvent + */ public function testSlashesExport() { $admin = User::factory()->administrator()->create(); diff --git a/tests/Feature/Events/InviteEventTest.php b/tests/Feature/Events/InviteEventTest.php index bb657e1128..cdaf35eac7 100644 --- a/tests/Feature/Events/InviteEventTest.php +++ b/tests/Feature/Events/InviteEventTest.php @@ -25,6 +25,7 @@ class InviteEventTest extends TestCase * Test notification content. * * @return void + * @story:PartyController::postSendInvite */ public function testInvite() { @@ -78,6 +79,13 @@ function ($notification, $channels, $user) use ($group, $event, $host) { assertEquals(0, $event->volunteers); } + /** + * @story:PartyController::postSendInvite + * @story:PartyController::view + * @story:PartyController::confirmInvite + * @story:PartyController::index + * @story:EventController::updateEventv2 + */ public function testInviteReal() { $userAttributes = $this->userAttributes(); @@ -186,6 +194,12 @@ public function testInviteReal() $response->assertSessionHas('warning'); } + /** + * @story:PartyController::getGroupEmailsWithNames + * @story:PartyController::postSendInvite + * @story:PartyController::confirmInvite + * @story:PartyController::view + */ public function testInvitableUserPOV() { $this->withoutExceptionHandling(); @@ -288,6 +302,12 @@ public function testInvitableUserPOV() $this->assertEquals([], $members); } + /** + * @story:PartyController::getGroupEmailsWithNames + * @story:PartyController::postSendInvite + * @story:PartyController::confirmInvite + * @story:PartyController::view + */ public function testInvitableNotifications() { Queue::fake(); @@ -402,6 +422,7 @@ function ($notification, $channels, $host) use ($user, $event) { }); } + /** @story:PartyController::confirmCodeInvite */ public function testInviteViaLink() { $this->loginAsTestUser(Role::ADMINISTRATOR); $user = User::factory()->restarter()->create([ @@ -445,6 +466,7 @@ public function testInviteViaLink() { $rsp = $this->get('/party/invite/' . $unique_shareable_code . '1'); } + /** @story:PartyController::postSendInvite */ public function testInviteNonUsers() { Notification::fake(); @@ -475,6 +497,7 @@ public function testInviteNonUsers() { $response->assertSessionHas('success'); } + /** @story:PartyController::postSendInvite */ public function testInviteNoUsers() { Notification::fake(); @@ -507,6 +530,7 @@ public function testInviteNoUsers() { /** * @dataProvider invalidEmailProvider + * @story:PartyController::postSendInvite */ public function testInviteInvalidEmail($email, $valid) { diff --git a/tests/Feature/Events/JoinEventTest.php b/tests/Feature/Events/JoinEventTest.php index e0a2d276a8..930d643776 100644 --- a/tests/Feature/Events/JoinEventTest.php +++ b/tests/Feature/Events/JoinEventTest.php @@ -14,6 +14,11 @@ class JoinEventTest extends TestCase { + /** + * @story:PartyController::getJoinEvent + * @story:PartyController::view + * @story:PartyController::cancelInvite + */ public function testJoin() { Queue::fake(); @@ -82,6 +87,7 @@ public function testJoin() }); } + /** @story:PartyController::getJoinEvent */ public function testJoinInvalid() { $user = User::factory()->restarter()->create(); $this->actingAs($user); diff --git a/tests/Feature/Events/ModerationEventPhotosNotificationTest.php b/tests/Feature/Events/ModerationEventPhotosNotificationTest.php index 28ae74009e..faa809d894 100644 --- a/tests/Feature/Events/ModerationEventPhotosNotificationTest.php +++ b/tests/Feature/Events/ModerationEventPhotosNotificationTest.php @@ -38,7 +38,11 @@ class ModerationEventPhotosNotificationTest extends TestCase */ protected $group; - /** @test */ + /** + * @test + * @story:PartyController::imageUpload + * @story:PartyController::deleteImage + */ public function a_moderation_notification_is_sent_to_admins_when_event_photos_are_uploaded() { Notification::fake(); diff --git a/tests/Feature/Events/OnlineEventsTest.php b/tests/Feature/Events/OnlineEventsTest.php index 4357c66830..b0aad4cf11 100644 --- a/tests/Feature/Events/OnlineEventsTest.php +++ b/tests/Feature/Events/OnlineEventsTest.php @@ -28,7 +28,10 @@ protected function setUp(): void }); } - /** @test */ + /** + * @test + * @story:EventController::createEventv2 + */ public function a_host_can_create_an_online_event() { $this->withoutExceptionHandling(); diff --git a/tests/Feature/Fixometer/BasicTest.php b/tests/Feature/Fixometer/BasicTest.php index e71d1d3b39..feaf1d7a20 100644 --- a/tests/Feature/Fixometer/BasicTest.php +++ b/tests/Feature/Fixometer/BasicTest.php @@ -16,6 +16,7 @@ class BasicTest extends TestCase { + /** @story:DeviceController::index */ public function testPageLoads() { // Create a past event with a fixed device. This is shown on the Fixometer page as the latest data. @@ -99,6 +100,7 @@ public function testPageLoads() $this->assertEquals($event->idevents, $data['idevents']); } + /** @story:ExportController::devices */ public function testExport() { $this->loginAsTestUser(Role::ADMINISTRATOR); diff --git a/tests/Feature/Groups/APIv2GroupTest.php b/tests/Feature/Groups/APIv2GroupTest.php index 23ce795642..42b06ac720 100644 --- a/tests/Feature/Groups/APIv2GroupTest.php +++ b/tests/Feature/Groups/APIv2GroupTest.php @@ -24,6 +24,8 @@ class APIv2GroupTest extends TestCase * @dataProvider providerTrueFalse * * @param $approve + * @story:GroupController::getGroupv2 + * @story:GroupController::moderateGroupsv2 */ public function testGetGroup($approve) { $user = User::factory()->administrator()->create([ @@ -92,6 +94,7 @@ public function providerTrueFalse() ]; } + /** @story:GroupController::createGroupv2 */ public function testCreateGroupLoggedOut() { $this->expectException(AuthenticationException::class); @@ -103,6 +106,7 @@ public function testCreateGroupLoggedOut() ]); } + /** @story:GroupController::createGroupv2 */ public function testCreateGroupLoggedInWithoutToken() { // Logged in as a user should work, even if we don't use an API token. @@ -122,6 +126,10 @@ public function testCreateGroupLoggedInWithoutToken() $this->assertTrue(array_key_exists('id', $json)); } + /** + * @story:GroupController::createGroupv2 + * @story:GroupController::listNamesv2 + */ public function testCreateGroupLoggedOutWithToken() { // Logged out should work if we use an API token. @@ -200,6 +208,7 @@ private function assertGroupFound($groups, $id, $shouldBeFound) { return $ix; } + /** @story:GroupController::createGroupv2 */ public function testCreateGroupGeocodeFailure() { $user = User::factory()->administrator()->create([ @@ -216,6 +225,7 @@ public function testCreateGroupGeocodeFailure() ]); } + /** @story:GroupController::createGroupv2 */ public function testCreateGroupInvalidTimezone() { $user = User::factory()->administrator()->create([ @@ -233,6 +243,7 @@ public function testCreateGroupInvalidTimezone() ]); } + /** @story:GroupController::createGroupv2 */ public function testCreateGroupDuplicate() { // Logged in as a user should work, even if we don't use an API token. @@ -259,6 +270,10 @@ public function testCreateGroupDuplicate() ]); } + /** + * @story:GroupController::listTagsv2 + * @story:GroupController::getGroupv2 + */ public function testTags() { $tag = GroupTags::factory()->create(); $response = $this->get('/api/v2/groups/tags', []); @@ -276,6 +291,7 @@ public function testTags() { self::assertEquals($tag->id, $json['data']['tags'][0]['id']); } + /** @story:GroupController::createGroupv2 */ public function testOutdated() { // Check we can create a group with an outdated timezone. $user = User::factory()->administrator()->create([ @@ -311,6 +327,7 @@ public function testOutdated() { * @dataProvider providerTrueFalse * * @return void + * @story:GroupController::moderateGroupsv2 */ public function testNetworkCoordinatorApprove($first) { $network1 = Network::factory()->create(); @@ -344,6 +361,7 @@ public function testNetworkCoordinatorApprove($first) { } } + /** @story:GroupController::getGroupv2 */ public function testLocales() { $user = User::factory()->administrator()->create([ 'api_token' => '1234', @@ -375,6 +393,10 @@ public function testLocales() { // Create a group in } + /** + * @story:GroupController::createGroupv2 + * @story:GroupController::getGroupv2 + */ public function testEmptyNetworkData() { $user = User::factory()->administrator()->create([ 'api_token' => '1234', @@ -410,6 +432,12 @@ public function testEmptyNetworkData() { assertEquals(null, $json['data']['network_data']); } + /** + * @story:GroupController::createGroupv2 + * @story:GroupController::updateGroupv2 + * @story:GroupController::getGroupv2 + * @story:GroupController::getGroupsByUsersNetworks + */ public function testNetworkDataUpdatedAt() { $user = User::factory()->administrator()->create([ 'api_token' => '1234', @@ -470,6 +498,11 @@ public function testNetworkDataUpdatedAt() { $this->assertEquals((new Carbon($updated_at))->getTimestamp(), (new Carbon($groups[0]['updated_at']))->getTimestamp()); } + /** + * @story:GroupController::getGroupv2 + * @story:GroupController::updateGroupv2 + * @story:GroupController::listNamesv2 + */ public function testArchived() { $user = User::factory()->administrator()->create([ 'api_token' => '1234', diff --git a/tests/Feature/Groups/BasicTest.php b/tests/Feature/Groups/BasicTest.php index 3af88c638f..2069f29ca5 100644 --- a/tests/Feature/Groups/BasicTest.php +++ b/tests/Feature/Groups/BasicTest.php @@ -14,6 +14,9 @@ class BasicTest extends TestCase { /** * @dataProvider tabProvider + * @story:GroupController::mine + * @story:GroupController::all + * @story:GroupController::nearby */ public function testPageLoads($url, $tab) { diff --git a/tests/Feature/Groups/GroupCreateTest.php b/tests/Feature/Groups/GroupCreateTest.php index 454a6b1a55..85a6adf8a8 100644 --- a/tests/Feature/Groups/GroupCreateTest.php +++ b/tests/Feature/Groups/GroupCreateTest.php @@ -17,6 +17,10 @@ class GroupCreateTest extends TestCase { + /** + * @story:GroupController::create + * @story:GroupController::getGroupList + */ public function testCreate() { $user = User::factory()->administrator()->create([ @@ -40,6 +44,7 @@ public function testCreate() self::assertEquals('dummy', $ret[0]['network_data']['dummy']); } + /** @story:GroupController::create */ public function testCreateGroupAsRestarter() { // Restarters can create groups. This wasn't true in the past and for backwards compatibility the act // of creation should convert them into a host. @@ -66,6 +71,7 @@ public function testCreateGroupAsRestarter() { $this->assertTrue($user->hasRole('Host')); } + /** @story:GroupController::create */ public function testCreateBadLocation() { $this->loginAsTestUser(Role::ADMINISTRATOR); @@ -84,6 +90,7 @@ public function roles() { /** * @dataProvider roles + * @story:GroupController::updateGroupv2 */ public function testApprove($role) { Notification::fake(); @@ -167,6 +174,7 @@ function ($notification, $channels, $host) use ($group) { } } + /** @story:GroupController::create */ public function testEventVisibility() { // Create a network. $network = Network::factory()->create(); diff --git a/tests/Feature/Groups/GroupDeleteTest.php b/tests/Feature/Groups/GroupDeleteTest.php index edf2c8dd93..427bf80b80 100644 --- a/tests/Feature/Groups/GroupDeleteTest.php +++ b/tests/Feature/Groups/GroupDeleteTest.php @@ -9,6 +9,7 @@ class GroupDeleteTest extends TestCase { + /** @story:GroupController::delete */ public function testDelete() { $this->loginAsTestUser(Role::ADMINISTRATOR); @@ -35,6 +36,7 @@ public function testDelete() ]), $response->getContent()); } + /** @story:GroupController::delete */ public function testCanDeleteWithEmptyEvent() { $this->loginAsTestUser(Role::ADMINISTRATOR); @@ -55,6 +57,7 @@ public function testCanDeleteWithEmptyEvent() ]), $response->getContent()); } + /** @story:GroupController::delete */ public function testCantDeleteWithDevice() { $this->loginAsTestUser(Role::ADMINISTRATOR); @@ -78,6 +81,7 @@ public function testCantDeleteWithDevice() $response->assertRedirect('/user/forbidden'); } + /** @story:GroupController::delete */ public function testCanDeleteWithDeletedEvent() { $this->loginAsTestUser(Role::ADMINISTRATOR); diff --git a/tests/Feature/Groups/GroupEditTest.php b/tests/Feature/Groups/GroupEditTest.php index c29ec12540..45d6acff86 100644 --- a/tests/Feature/Groups/GroupEditTest.php +++ b/tests/Feature/Groups/GroupEditTest.php @@ -17,7 +17,11 @@ class GroupEditTest extends TestCase { - /** @test */ + /** + * @test + * @story:GroupController::edit + * @story:GroupController::updateGroupv2 + */ public function group_tags_retained_after_edited_by_host() { $this->withoutExceptionHandling(); @@ -58,6 +62,7 @@ public function group_tags_retained_after_edited_by_host() ]); } + /** @story:GroupController::edit */ public function testEditGroupAsRestarter() { $group = Group::factory()->create(); @@ -66,7 +71,10 @@ public function testEditGroupAsRestarter() { $this->get('/group/edit/' . $group->idgroups); } - /** @test */ + /** + * @test + * @story:GroupController::updateGroupv2 + */ public function invalid_location() { $this->withoutExceptionHandling(); @@ -92,7 +100,11 @@ public function invalid_location() ]); } - /** @test */ + /** + * @test + * @story:GroupController::imageUpload + * @story:GroupController::ajaxDeleteImage + */ public function image_upload() { Storage::fake('avatars'); $group = Group::factory()->create(); @@ -157,7 +169,11 @@ public function can_edit_timezone() { self::assertTrue($found); } - /** @test */ + /** + * @test + * @story:GroupController::edit + * @story:GroupController::updateGroupv2 + */ public function edit_email() { $this->withoutExceptionHandling(); @@ -191,6 +207,7 @@ public function edit_email() $this->assertEquals('info@test.com', $group->email); } + /** @story:GroupController::edit */ public function testEditAsNetworkCoordinator() { $network = Network::factory()->create(); $coordinator = User::factory()->restarter()->create(); diff --git a/tests/Feature/Groups/GroupHostTest.php b/tests/Feature/Groups/GroupHostTest.php index a8f1ebfd19..589fadaded 100644 --- a/tests/Feature/Groups/GroupHostTest.php +++ b/tests/Feature/Groups/GroupHostTest.php @@ -36,6 +36,9 @@ public function roleProvider() { /** * @dataProvider roleProvider + * @story:GroupController::getVolunteersForGroupv2 + * @story:GroupController::patchVolunteerForGroupv2 + * @story:GroupController::deleteVolunteerForGroupv2 */ public function testMakeHost($role) { @@ -95,6 +98,10 @@ public function testMakeHost($role) $this->assertEquals(1, count($json['data'])); } + /** + * @story:GroupController::patchVolunteerForGroupv2 + * @story:GroupController::deleteVolunteerForGroupv2 + */ public function testHostMakeHost() { $firsthost = User::factory()->host()->create(); @@ -128,6 +135,7 @@ public function providerTrueFalse() /** * @dataProvider providerTrueFalse + * @story:GroupController::patchVolunteerForGroupv2 */ public function testNetworkCoordinatorDemoteHost($addToNetwork) { $host = User::factory()->host()->create(); diff --git a/tests/Feature/Groups/GroupJoinTest.php b/tests/Feature/Groups/GroupJoinTest.php index 7a440f5af2..15425087a0 100644 --- a/tests/Feature/Groups/GroupJoinTest.php +++ b/tests/Feature/Groups/GroupJoinTest.php @@ -14,6 +14,10 @@ class GroupJoinTest extends TestCase { + /** + * @story:GroupController::getJoinGroup + * @story:UserGroupsController::leave + */ public function testJoin() { Notification::fake(); diff --git a/tests/Feature/Groups/GroupNetworkCreateTest.php b/tests/Feature/Groups/GroupNetworkCreateTest.php index 439a768fd7..e059cc7dec 100644 --- a/tests/Feature/Groups/GroupNetworkCreateTest.php +++ b/tests/Feature/Groups/GroupNetworkCreateTest.php @@ -24,7 +24,10 @@ class GroupNetworkCreateTest extends TestCase { // New group is created as part of the network represented by the current domain. - /** @test */ + /** + * @test + * @story:GroupController::createGroupv2 + */ public function given_specific_domain_when_group_created_then_it_is_created_as_part_of_corresponding_network() { $this->withoutExceptionHandling(); diff --git a/tests/Feature/Groups/GroupTagsTest.php b/tests/Feature/Groups/GroupTagsTest.php index da3bc3171a..7dcccf5dce 100644 --- a/tests/Feature/Groups/GroupTagsTest.php +++ b/tests/Feature/Groups/GroupTagsTest.php @@ -15,6 +15,7 @@ class GroupTagsTest extends TestCase { + /** @story:GroupTagsController::index */ public function testList() { $admin = $this->loginAsTestUser(Role::RESTARTER); @@ -29,6 +30,7 @@ public function testList() $response->assertSeeText($tag->tag_name); } + /** @story:GroupTagsController::postCreateTag */ public function testCreate() { $tag = GroupTags::factory()->create(); @@ -50,6 +52,7 @@ public function testCreate() $response->assertSessionHas('success'); } + /** @story:GroupTagsController::getEditTag */ public function testGetEdit() { $tag = GroupTags::factory()->create(); @@ -65,6 +68,7 @@ public function testGetEdit() $response->assertSeeText($tag->tag_name); } + /** @story:GroupTagsController::postEditTag */ public function testEdit() { $tag = GroupTags::factory()->create(); @@ -86,6 +90,7 @@ public function testEdit() $response->assertSessionHas('success'); } + /** @story:GroupTagsController::getDeleteTag */ public function testDelete() { $tag = GroupTags::factory()->create(); diff --git a/tests/Feature/Groups/GroupViewTest.php b/tests/Feature/Groups/GroupViewTest.php index 63b7eec934..c5f3e4e696 100644 --- a/tests/Feature/Groups/GroupViewTest.php +++ b/tests/Feature/Groups/GroupViewTest.php @@ -12,6 +12,7 @@ class GroupViewTest extends TestCase { + /** @story:GroupController::view */ public function testBasic() { // Check we can create a group and view it. @@ -53,6 +54,7 @@ public function testBasic() $this->assertEquals(1, count(json_decode($props[1][':events'], TRUE))); } + /** @story:GroupController::view */ public function testInvalidGroup() { $this->loginAsTestUser(Role::RESTARTER); @@ -60,6 +62,7 @@ public function testInvalidGroup() $this->get('/group/view/undefined'); } + /** @story:GroupController::view */ public function testInvalidGroup2() { $this->loginAsTestUser(Role::RESTARTER); @@ -67,6 +70,11 @@ public function testInvalidGroup2() $this->get('/group/view/1'); } + /** + * @story:GroupController::view + * @story:ApiController::getDevices + * @story:OutboundController::info + */ public function testCanDelete() { $this->loginAsTestUser(Role::ADMINISTRATOR); @@ -139,6 +147,7 @@ public function testCanDelete() } } + /** @story:GroupController::view */ public function testInProgressVisible() { $this->loginAsTestUser(Role::ADMINISTRATOR); $id = $this->createGroup(); diff --git a/tests/Feature/Groups/InviteGroupTest.php b/tests/Feature/Groups/InviteGroupTest.php index 8235d62b4c..a4840ab6bf 100644 --- a/tests/Feature/Groups/InviteGroupTest.php +++ b/tests/Feature/Groups/InviteGroupTest.php @@ -18,6 +18,11 @@ class InviteGroupTest extends TestCase { + /** + * @story:GroupController::postSendInvite + * @story:GroupController::view + * @story:GroupController::confirmInvite + */ public function testInvite() { Notification::fake(); @@ -131,6 +136,10 @@ function ($notification, $channels, $host) use ($group, $user) { ); } + /** + * @story:GroupController::view + * @story:GroupController::confirmCodeInvite + */ public function testInviteViaLink() { $group = Group::factory()->create(); @@ -159,6 +168,7 @@ public function testInviteViaLink() { /** * @dataProvider invalidEmailProvider + * @story:GroupController::postSendInvite */ public function testInviteInvalidEmail($email, $valid) { diff --git a/tests/Feature/Home/HomeTest.php b/tests/Feature/Home/HomeTest.php index 3e543f1bbd..4baf0f0512 100644 --- a/tests/Feature/Home/HomeTest.php +++ b/tests/Feature/Home/HomeTest.php @@ -11,6 +11,7 @@ class HomeTest extends TestCase { /** + * @story:HomeController::index * @dataProvider landingPagesProvider */ public function testLoggedOut($url) @@ -29,6 +30,7 @@ public function landingPagesProvider() { ]; } + /** @story:HomeController::index */ public function testLoggedIn() { $this->loginAsTestUser(Role::RESTARTER); $response = $this->get('/user'); diff --git a/tests/Feature/Networks/APIv2NetworkTest.php b/tests/Feature/Networks/APIv2NetworkTest.php index a270b73e16..b6b1dc8bee 100644 --- a/tests/Feature/Networks/APIv2NetworkTest.php +++ b/tests/Feature/Networks/APIv2NetworkTest.php @@ -13,6 +13,7 @@ class APIv2NetworkTest extends TestCase { + /** @story:NetworkController::getNetworksv2 */ public function testList() { $user = User::factory()->administrator()->create([ 'api_token' => '1234', @@ -44,6 +45,10 @@ public function testList() { self::assertTrue($found); } + /** + * @story:NetworkController::getNetworkv2 + * @story:NetworkController::getNetworksv2 + */ public function testGet() { $network = Network::first(); self::assertNotNull($network); @@ -76,6 +81,7 @@ public function testGet() { } /** + * @story:NetworkController::getNetworkGroupsv2 * @dataProvider providerGroupsParameters * @param $value */ @@ -156,6 +162,7 @@ public function providerGroupsParameters() { } /** + * @story:NetworkController::getNetworkEventsv2 * @dataProvider providerEventsParameters * @param $value */ diff --git a/tests/Feature/Networks/NetworkTest.php b/tests/Feature/Networks/NetworkTest.php index 26415767b0..5fe05c5edf 100644 --- a/tests/Feature/Networks/NetworkTest.php +++ b/tests/Feature/Networks/NetworkTest.php @@ -81,7 +81,12 @@ public function groups_can_be_associated_to_network() $this->assertTrue($group->isMemberOf($network)); } - /** @test */ + /** + * @test + * @story:NetworkController::associateGroup + * @story:GroupController::getGroupsByUsersNetworks + * @story:EventController::getEventsByUsersNetworks + */ public function admins_can_associate_group_to_network() { $this->withoutExceptionHandling(); @@ -211,7 +216,10 @@ public function user_can_be_set_as_coordinator_of_network() $this->assertTrue($coordinator->isCoordinatorOf($network)); } - /** @test */ + /** + * @test + * @story:NetworkController::stats + */ public function network_stats_can_be_queried() { $network = Network::factory()->create(); @@ -234,7 +242,13 @@ public function network_stats_can_be_queried() $this->assertEquals($expectedStats, $stats); } - /** @test */ + /** + * @test + * @story:NetworkController::index + * @story:NetworkController::show + * @story:NetworkController::associateGroup + * @story:GroupController::network + */ public function network_page() { $network = Network::factory()->create([ @@ -296,7 +310,12 @@ public function network_page() $response->assertSee(__('networks.index.all_networks_explainer')); } - /** @test */ + /** + * @test + * @story:NetworkController::edit + * @story:NetworkController::update + * @story:NetworkController::associateGroup + */ public function admins_can_edit() { $this->withoutExceptionHandling(); @@ -327,6 +346,7 @@ public function admins_can_edit() $this->assertTrue($group->isMemberOf($network)); } + /** @story:UserController::postAdminEdit */ public function testRemoveNetworkCoordinatorByRole() { $this->withoutExceptionHandling(); diff --git a/tests/Feature/Notifications/BasicTest.php b/tests/Feature/Notifications/BasicTest.php index cb7e007370..c777623e2b 100644 --- a/tests/Feature/Notifications/BasicTest.php +++ b/tests/Feature/Notifications/BasicTest.php @@ -15,6 +15,7 @@ class BasicTest extends TestCase { + /** @story:UserController::getNotifications */ public function testNotificationsPage() { $this->loginAsTestUser(Role::ADMINISTRATOR); $idgroups = $this->createGroup(); diff --git a/tests/Feature/Role/RoleTest.php b/tests/Feature/Role/RoleTest.php index a29fd58189..c6586b9f7d 100644 --- a/tests/Feature/Role/RoleTest.php +++ b/tests/Feature/Role/RoleTest.php @@ -12,17 +12,23 @@ class RoleTest extends TestCase { + /** @story:RoleController::index */ public function testLoggedOut() { $this->expectException(AuthenticationException::class); $response = $this->get('/role'); } + /** @story:RoleController::index */ public function testNotAdmin() { $this->loginAsTestUser(Role::RESTARTER); $response = $this->get('/role'); $response->assertRedirect(RouteServiceProvider::HOME); } + /** + * @story:RoleController::index + * @story:RoleController::edit + */ public function testBasic() { $this->loginAsTestUser(Role::ADMINISTRATOR); diff --git a/tests/Feature/Stats/EventStatsTest.php b/tests/Feature/Stats/EventStatsTest.php index a683f4f9a7..0f4dd4c5e0 100644 --- a/tests/Feature/Stats/EventStatsTest.php +++ b/tests/Feature/Stats/EventStatsTest.php @@ -20,7 +20,11 @@ public function an_event_with_no_devices_has_empty_stats() $this->assertEquals($expect, $event->getEventStats()); } - /** @test */ + /** + * @test + * @story:AdminController::stats + * @story:PartyController::stats + */ public function event_stats_with_both_powered_and_unpowered_devices() { $this->_setupCategoriesWithUnpoweredWeights(); @@ -180,7 +184,11 @@ public function event_stats_with_both_powered_and_unpowered_devices() $response->assertSee('23', false); } - /** @test */ + /** + * @test + * @story:PartyController::view + * @story:GroupController::view + */ public function event_stats_for_upcoming_event() { $this->_setupCategoriesWithUnpoweredWeights(); diff --git a/tests/Feature/Stats/GroupStatsTest.php b/tests/Feature/Stats/GroupStatsTest.php index 0f624604df..6f10a40fad 100644 --- a/tests/Feature/Stats/GroupStatsTest.php +++ b/tests/Feature/Stats/GroupStatsTest.php @@ -20,7 +20,10 @@ public function a_group_with_no_events_has_empty_stats() $this->assertEquals($expect, $group->getGroupStats()); } - /** @test */ + /** + * @test + * @story:GroupController::stats + */ public function a_group_with_one_past_event_has_stats_for_that_event() { $group = Group::factory()->create(); @@ -340,7 +343,10 @@ public function two_groups_with_mixed_devices_have_correct_stats() } } - /** @test */ + /** + * @test + * @story:ApiController::groupStats + */ public function get_of_stats_after_deletion() { $admin = User::factory()->administrator()->create([ diff --git a/tests/Feature/Users/EditLanguageSettingsTest.php b/tests/Feature/Users/EditLanguageSettingsTest.php index 8c569d8039..5205c8c76a 100644 --- a/tests/Feature/Users/EditLanguageSettingsTest.php +++ b/tests/Feature/Users/EditLanguageSettingsTest.php @@ -48,7 +48,10 @@ public function user_language_update_triggers_language_sync() Event::assertDispatched(UserLanguageUpdated::class); } - /** @test */ + /** + * @test + * @story:LocaleController::setLang + */ // Added these to try (and fail) to reproduce a Sentry error. public function user_sets_language() { diff --git a/tests/Feature/Users/EditProfileTest.php b/tests/Feature/Users/EditProfileTest.php index c945072063..629c9b3f13 100644 --- a/tests/Feature/Users/EditProfileTest.php +++ b/tests/Feature/Users/EditProfileTest.php @@ -89,7 +89,10 @@ public function test_three_digit_lat_lng() $this->assertEquals(132.654, $user->longitude); } - /** test */ + /** + * test + * @story:UserController::postProfileInfoEdit + */ // Check that we can update the location. public function test_location_update() { @@ -138,6 +141,8 @@ public function idProvider() { /** * @test * @dataProvider idProvider + * @story:UserController::postProfileTagsEdit + * @story:UserController::getProfileEdit */ public function test_tags_update($id) { $user = User::factory()->create(); @@ -185,6 +190,7 @@ public function test_tags_update($id) { /** * @test * @dataProvider idProvider + * @story:UserController::postProfilePictureEdit */ public function image_upload($id) { Storage::fake('avatars'); @@ -225,6 +231,7 @@ public function image_upload($id) { /** * @test + * @story:UserController::postProfileInfoEdit */ public function edit_profile() { $user = User::factory()->create(); diff --git a/tests/Feature/Users/GroupsNearbyTest.php b/tests/Feature/Users/GroupsNearbyTest.php index 73bc9c7bea..0561f798c1 100644 --- a/tests/Feature/Users/GroupsNearbyTest.php +++ b/tests/Feature/Users/GroupsNearbyTest.php @@ -113,6 +113,7 @@ public function testInactive() $this->assertEquals(0, count($groups)); } + /** @story:GroupController::updateGroupv2 */ public function testNotification() { Notification::fake(); diff --git a/tests/Feature/Users/PasswordResetTest.php b/tests/Feature/Users/PasswordResetTest.php index c67bf847a4..fab1a24076 100644 --- a/tests/Feature/Users/PasswordResetTest.php +++ b/tests/Feature/Users/PasswordResetTest.php @@ -11,6 +11,7 @@ class PasswordResetTest extends TestCase { + /** @story:UserController::recover */ public function testInvalidEmail() { $response = $this->post('/user/recover', [ 'email' => 'bademail!' @@ -19,6 +20,7 @@ public function testInvalidEmail() { $response->assertSeeText(__('passwords.invalid')); } + /** @story:UserController::recover */ public function testUnknownEmail() { $response = $this->post('/user/recover', [ 'email' => 'nobody@nowhere.com' @@ -27,6 +29,10 @@ public function testUnknownEmail() { $response->assertSeeText(__('passwords.user'), false); } + /** + * @story:UserController::recover + * @story:UserController::reset + */ public function testResetSuccess() { Notification::fake(); diff --git a/tests/Feature/Users/ProfileTest.php b/tests/Feature/Users/ProfileTest.php index 22535fe544..18f4b197e5 100644 --- a/tests/Feature/Users/ProfileTest.php +++ b/tests/Feature/Users/ProfileTest.php @@ -12,6 +12,7 @@ class ProfileTest extends TestCase { + /** @story:UserController::index */ public function testProfilePage() { $user = User::factory()->restarter()->create(); @@ -36,6 +37,7 @@ public function testProfilePage() $response->assertSee(__('profile.my_skills')); } + /** @story:UserController::edit */ public function testEdit() { $GLOBALS['_FILES'] = []; @@ -98,6 +100,7 @@ public function testEdit() $response->assertSee('Edit User'); } + /** @story:UserController::edit */ public function testEditBadPassword() { $GLOBALS['_FILES'] = []; @@ -125,6 +128,7 @@ public function testBadMediaWikiId() $this->get('/user/thumbnail?wiki_username=invalid'); } + /** @story:UserController::postProfilePasswordEdit */ public function testChangePassword() { $user = User::factory()->restarter()->create(); $user->setPassword(Hash::make('secret1')); @@ -144,6 +148,7 @@ public function testChangePassword() { $this->assertEquals(__('profile.password_changed'), \Session::get('message')); } + /** @story:UserController::postProfileRepairDirectory */ public function testRepairDirectoryRole() { $user = User::factory()->restarter()->create(); $admin = User::factory()->administrator()->create([ @@ -160,6 +165,7 @@ public function testRepairDirectoryRole() { $this->assertEquals(__('profile.profile_updated'), \Session::get('message')); } + /** @story:UserController::storeLanguage */ public function testLanguage() { $user = User::factory()->restarter()->create(); $this->actingAs($user); @@ -174,6 +180,7 @@ public function testLanguage() { /** * @dataProvider invitesProvider + * @story:UserController::postProfilePreferencesEdit */ public function testInvites($admin, $invites) { $user = User::factory()->restarter()->create(); @@ -208,6 +215,10 @@ public function invitesProvider() { ]; } + /** + * @story:ApiController::getUserInfo + * @story:ApiController::getUserList + */ public function testAPI() { $user = User::factory()->administrator()->create([ 'api_token' => '1234', diff --git a/tests/Feature/Users/RecoverTest.php b/tests/Feature/Users/RecoverTest.php index b277a19f71..73e09f13e3 100644 --- a/tests/Feature/Users/RecoverTest.php +++ b/tests/Feature/Users/RecoverTest.php @@ -23,6 +23,12 @@ private function getCode($recovery) { } } + /** + * @story:UserController::recover + * @story:UserController::reset + * @story:LoginController::showLoginForm + * @story:LoginController::login + */ public function testRecover() { $restarter = User::factory()->restarter()->create([ diff --git a/tests/Feature/Users/Registration/AccountCreationTest.php b/tests/Feature/Users/Registration/AccountCreationTest.php index 1d3ae2e2b6..e56f9d8313 100644 --- a/tests/Feature/Users/Registration/AccountCreationTest.php +++ b/tests/Feature/Users/Registration/AccountCreationTest.php @@ -13,6 +13,11 @@ class AccountCreationTest extends TestCase { + /** + * @story:UserController::getRegister + * @story:UserController::postRegister + * @story:UserController::getOnboardingComplete + */ public function testRegister() { $response = $this->get('/user/register'); @@ -92,6 +97,7 @@ public function testRegisterAgain() $this->assertEquals(1950, \Auth::user()->age); } + /** @story:UserController::logout */ public function testLogout() { $response = $this->post('/user/register/', $this->userAttributes()); @@ -148,6 +154,7 @@ public function testValidEmail() $this->assertNull(json_decode($response->getContent(), true)); } + /** @story:UserController::create */ public function testAdminCreate() { $this->loginAsTestUser(Role::ADMINISTRATOR); diff --git a/tests/Feature/Users/SkillsTest.php b/tests/Feature/Users/SkillsTest.php index 275e51bcde..247b1f1a43 100644 --- a/tests/Feature/Users/SkillsTest.php +++ b/tests/Feature/Users/SkillsTest.php @@ -12,6 +12,7 @@ class SkillsTest extends TestCase { + /** @story:SkillsController::index */ public function testIndex() { $this->loginAsTestUser(Role::RESTARTER); @@ -29,6 +30,10 @@ public function testIndex() { $response->assertSee('UT1'); } + /** + * @story:SkillsController::postCreateSkill + * @story:SkillsController::index + */ public function testCreate() { $this->loginAsTestUser(Role::RESTARTER); @@ -48,6 +53,11 @@ public function testCreate() { $response->assertSee('UT1'); } + /** + * @story:SkillsController::getEditSkill + * @story:SkillsController::postEditSkill + * @story:SkillsController::index + */ public function testEdit() { $this->loginAsTestUser(Role::RESTARTER); @@ -78,6 +88,7 @@ public function testEdit() { $response->assertSee('UT2'); } + /** @story:SkillsController::getDeleteSkill */ public function testDelete() { $this->loginAsTestUser(Role::RESTARTER); diff --git a/tests/Feature/Users/UserAdminTest.php b/tests/Feature/Users/UserAdminTest.php index 83683d2b39..c08b0d7553 100644 --- a/tests/Feature/Users/UserAdminTest.php +++ b/tests/Feature/Users/UserAdminTest.php @@ -32,7 +32,8 @@ public function provider() } /** - *@dataProvider provider + * @dataProvider provider + * @story:UserController::all */ public function testUsersPage($role, $cansee) { @@ -50,6 +51,7 @@ public function testUsersPage($role, $cansee) } } + /** @story:UserController::postSoftDeleteUser */ public function testSoftDelete() { $user = User::factory()->restarter()->create(); $this->loginAsTestUser(Role::ADMINISTRATOR); diff --git a/tests/Feature/Zapier/ZapierNetworkTests.php b/tests/Feature/Zapier/ZapierNetworkTests.php index ff6bf9facd..0b50c99541 100644 --- a/tests/Feature/Zapier/ZapierNetworkTests.php +++ b/tests/Feature/Zapier/ZapierNetworkTests.php @@ -43,7 +43,11 @@ protected function setUp(): void // When a new group is created that is in the Restart network, it IS included in the Restart Zapier trigger - /** @test */ + /** + * @test + * @story:GroupController::createGroupv2 + * @story:GroupController::getGroupChanges + */ public function given_restart_network_when_new_group_created_included_in_trigger() { $this->withoutExceptionHandling(); @@ -81,7 +85,11 @@ public function given_restart_network_when_new_group_created_included_in_trigger // When a new group is created that is in Repair Together network, it is not included in the Restart Zapier trigger - /** @test */ + /** + * @test + * @story:GroupController::createGroupv2 + * @story:GroupController::getGroupChanges + */ public function given_nonrestart_network_when_new_group_created_not_included_in_trigger() { $this->withoutExceptionHandling(); @@ -119,7 +127,10 @@ public function given_nonrestart_network_when_new_group_created_not_included_in_ // When a new user is created that is in the Restart network, it IS included in the Restart Zapier trigger - /** @test */ + /** + * @test + * @story:UserController::changes + */ public function given_restart_network_when_new_user_created_then_included_in_trigger() { $this->withoutExceptionHandling(); @@ -147,7 +158,10 @@ public function given_restart_network_when_new_user_created_then_included_in_tri // When a new user is created that is in the Repair Together network, it is not included in the Restart Zapier trigger - /** @test */ + /** + * @test + * @story:UserController::changes + */ public function given_nonrestart_network_when_new_user_created_then_not_included_in_trigger() { $this->withoutExceptionHandling(); @@ -175,7 +189,10 @@ public function given_nonrestart_network_when_new_user_created_then_not_included // When a new user/group association is created for a user in the Restart network joining a group in the Restart network, it IS included in the Restart Zapier trigger - /** @test */ + /** + * @test + * @story:UserGroupsController::changes + */ public function given_restart_group_and_restart_user_when_user_joins_group_then_included_in_trigger() { $this->withoutExceptionHandling(); @@ -206,7 +223,10 @@ public function given_restart_group_and_restart_user_when_user_joins_group_then_ // When a new user/group association is created and either the user or the group is not in the Restart network, it isn't included in the Restart Zapier trigger - /** @test */ + /** + * @test + * @story:UserGroupsController::changes + */ public function given_nonrestart_group_or_nonrestart_user_when_user_joins_group_then_not_included_in_trigger() { $this->withoutExceptionHandling(); diff --git a/tests/Unit/CharsetTest.php b/tests/Unit/CharsetTest.php index d14f9be689..319c6a6f27 100644 --- a/tests/Unit/CharsetTest.php +++ b/tests/Unit/CharsetTest.php @@ -21,7 +21,10 @@ protected function setUp(): void parent::setUp(); } - /** @test */ + /** + * @test + * @story:PartyController::view + */ public function test_charset_db_insert() { DB::statement('SET foreign_key_checks=0'); diff --git a/tests/Unit/Events/EventStateTest.php b/tests/Unit/Events/EventStateTest.php index cace9b55e4..9141e433c8 100644 --- a/tests/Unit/Events/EventStateTest.php +++ b/tests/Unit/Events/EventStateTest.php @@ -72,6 +72,8 @@ public function is_doesnt_start_too_soon() { /** * @dataProvider timeProvider + * @story:EventController::createEventv2 + * @story:PartyController::view */ public function testStatesOnViewPage($date, $upcoming, $finished, $inprogress, $startingsoon) { diff --git a/tests/Unit/Events/TimezoneTest.php b/tests/Unit/Events/TimezoneTest.php index 543468d3e7..cf02021194 100644 --- a/tests/Unit/Events/TimezoneTest.php +++ b/tests/Unit/Events/TimezoneTest.php @@ -75,6 +75,9 @@ public function testStartEnd() { /** * @dataProvider timesProvider + * @story:EventController::createEventv2 + * @story:PartyController::index + * @story:EventController::updateEventv2 */ public function testOrder($date, $tz1, $start1, $end1, $tz2, $start2, $end2, $editstart2, $editend2) { // Two groups in different timezones. @@ -187,6 +190,9 @@ public function testOldEndFieldException() { $p->end = '10:00'; } + /** @story:EventController::createEventv2 + * @story:GroupController::updateGroupv2 + */ public function testTimezoneChangeUpdatesFutureEvents() { // Create a group. $g = Group::factory()->create([