diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f755c9a..4144eaa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,4 +9,9 @@ updates: - package-ecosystem: "npm" directory: "/" schedule: - interval: "daily" \ No newline at end of file + interval: "daily" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml new file mode 100644 index 0000000..e168ff6 --- /dev/null +++ b/.github/workflows/analysis.yml @@ -0,0 +1,48 @@ +name: Code Analysis + +on: + push: + pull_request: + +jobs: + code_analysis_composer: + timeout-minutes: 30 + if: github.event_name == 'pull_request' + name: ${{ matrix.actions.name }} on PHP ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [8.1"] + + actions: + - name: PHPStan + run: "composer phpstan" + + - name : Composer validate + run : "composer validate" + + env: + APP_NAME : "Worksome CI" + APP_ENV : testing + FEATURE_FLAGS_GRAPHQL_V2: true + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + coverage: none + php-version: ${{ matrix.php }} + tools: composer, cs2pr + + - name: Copy .env file + run: cp .env.example .env + + + - name: Install Composer dependencies + uses: ramsey/composer-install@v2 + + - run: ${{ matrix.actions.run }} \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bf9ba76..7788814 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,7 @@ name: Tests on: push: + pull_request: jobs: setup: @@ -23,5 +24,17 @@ jobs: - name: Install Composer dependencies uses: ramsey/composer-install@v2 + - name: Copy .env file + run: cp .env.example .env + + - name: Prepare the application + run: php artisan key:generate + + - name: Clear Config + run: php artisan config:clear + + - name: Generate keys for Passport + run: php artisan passport:keys + - name: Test php code run: php artisan test diff --git a/app/Actions/CalculateResult.php b/app/Actions/CalculateResult.php new file mode 100644 index 0000000..c97af20 --- /dev/null +++ b/app/Actions/CalculateResult.php @@ -0,0 +1,283 @@ +getVoterDetails)($election); + $nBallots = $this->buildNBallots($election); + Log::debug('BALLOTS: ' . self::ballotsToText($nBallots)); + if (count($nBallots) == 0) { + $errorMessage = "there were 0 ballots ready for the election"; + throw new \Exception($errorMessage); + } + $tieBreaker = $nBallots[array_rand($nBallots)]; + $tieBreakerTotal = self::createTotallyOrderedBallot($tieBreaker); + + // calculated results + $calculator = new RankedPairsCalculator($tieBreakerTotal); + $numWinners = $election->candidates()->count(); + $result = $calculator->calculate($numWinners, null, ...$nBallots); + $tidemanWinners = $result->getRanking(); + + // translate tideman candidate objects back to pivot candidate objects + $pivotCandidates = $election->candidates()->get()->keyBy('id'); + $pivotWinners = array_map(function($tidemanCandidateList) use ($pivotCandidates){ + return array_map(function($tidemanCandidate) use ($pivotCandidates){ + $candidateId = $tidemanCandidate->getId(); + return $pivotCandidates[$candidateId]; + }, $tidemanCandidateList->toArray()); + }, $tidemanWinners->toArray()); + } catch (\Exception $e) { + // for debug blob + $exceptionMessage = $e->getMessage(); + $exceptionStack = $e->getTraceAsString(); + } + + // populate snapshot blob (json): debug, debug_private, and order + $debug = array(); + $debug["ballots"] = array_map('self::ballotToText', $nBallots); + $debug["tie_breaker"] = is_null($tieBreaker) ? null : self::ballotToText($tieBreaker); + $debug["tie_breaker_total"] = is_null($tieBreakerTotal) ? null : self::ballotToText($tieBreakerTotal); + $debug["election_config"] = $election->config; + $debug["exception"] = array("message" => $exceptionMessage, "stack" => $exceptionStack); + $result = array(); + $debug_private = array(); + $debug_private["candidates"] = array(); + foreach ($pivotCandidates as $c) { + $debug_private["candidates"][] = ["id" => $c->id, "name" => $c->name]; + } + $debug_private["electors"] = array(); + foreach ($pivotElectors as $state => $electors) { + foreach ($electors as $e) { + $row = array("id"=>$e["elector_id"], + "name"=>$e["voter_name"] ?: $e["user_name"], + "email"=>$e["email"], + "state"=>$state); + $debug_private["electors"][] = $row; + } + } + + // top-level fields in snapshot blob + $result["error"] = $errorMessage; + $result["debug"] = $debug; + $result["debug_private"] = $debug_private; + $result["order"] = $pivotWinners; + + return $result; + } + + public static function ballotToText($nBallot): string + { + return implode(">", + array_map(function($candidateList) { + return implode("=", + array_map(function($candidate) { + return $candidate->getId(); + }, $candidateList->toArray())); + }, $nBallot->toArray())); + } + + public static function ballotsToText($nBallots): string + { + $lines = []; + foreach ($nBallots as $nBallot) { + $lines[] = self::ballotToText($nBallot); + } + return implode("\n", $lines); + } + + public function createTotallyOrderedBallot($nBallot): Ballot + { + $totalOrder = []; + foreach ($nBallot as $candidateList) { + $candidates = (clone $candidateList)->toArray(); + shuffle($candidates); + array_push($totalOrder, ...$candidates); + } + + // every candidate should be in its own CandidateList + $totalOrder = array_map(function($candidate){ + return new CandidateList($candidate); + }, $totalOrder); + + return new Ballot(...$totalOrder); + } + + /** + * @return array of NBallots + */ + public function buildNBallots(Election $election): array + { + $candidateRanks = $this->getCandidateRankCollection($election); + $candidateRanksGroupedByElectorAndRank = $this->groupRankingsByElectorAndRank($candidateRanks); + + return array_map(function($ballotArray){ + $candidateLists = array_map(function($candidateListArray){ + + $candidates = array_map(function($candidateArray){ + return new TidemanCandidate($candidateArray->candidate_id, $candidateArray->name); + }, $candidateListArray); + + return new CandidateList(...$candidates); + }, $ballotArray); + + # sort by rank (best=1 first) + ksort($candidateLists); + + return new NBallot(1, ...$candidateLists); + }, $candidateRanksGroupedByElectorAndRank); + } + + /** + * @param Collection $candidateRanks + * @return array of arrays of arrays. The arrays are grouped at the outermost level + * on elector id. The arrays are grouped at the next level by rank. Array entries at this level are ordered in + * ascending rank. The innermost arrays are associative arrays that contain CandidateRank attributes. + */ + public function groupRankingsByElectorAndRank($candidateRanks): array + { + // determine largest (worst) rank + $max_rank = 1; + $ranks = array_map(function($candidateRank){return $candidateRank->rank;}, $candidateRanks->toArray()); + if (count($ranks) > 0) { + $max_rank = max($ranks); + } + $unranked = $max_rank + 1; + + // bucketize by elector + $candidateRanksGroupedByElector = $candidateRanks->mapToGroups(function($candidateRank){ + $key = $candidateRank->elector_id; + $value = $candidateRank; + return [ $key => $value ]; + }); + + // bucketize by rank within each elector + $candidateRanksGroupedByElectorAndRank = $candidateRanksGroupedByElector->map( + fn($candidateRanksFromOneElector) => $candidateRanksFromOneElector->mapToGroups(function($candidateRank) use ($unranked) { + // default rank is <= 0. Map that to largest value + $key = (is_null($candidateRank->rank) || $candidateRank->rank <= 0) ? $unranked : $candidateRank->rank; + $value = $candidateRank; + return [ $key => $value ]; + }) + )->toArray(); + return $candidateRanksGroupedByElectorAndRank; + } + + /** + * @return \Illuminate\Support\Collection of all CandidateRanks associated with + * the election identified by the parameterized id. + */ + public function getCandidateRankCollection(Election $election) + { + $electionId = $election->getKey(); + $query = DB::table('elections')->where('elections.id', '=', $electionId) + ->join('candidates', 'candidates.election_id', '=', 'elections.id') + ->join('electors', 'electors.election_id', '=', 'elections.id') + ->leftJoin('candidate_ranks', function($join) { + $join->on('candidate_ranks.elector_id', '=', 'electors.id'); + $join->on('candidate_ranks.candidate_id', '=', 'candidates.id'); + }); + if ($this->getConfigApprovedOnly($election)) { + $query = $query->where('electors.ballot_version_approved', '=', DB::raw('elections.ballot_version')); + } + $query = $query->select('electors.id AS elector_id', 'candidates.id AS candidate_id', 'candidates.name', 'candidate_ranks.rank'); + + Log::debug('BALLOT QUERY: ' . $query->toSql()); + + return $query->get(); + } + + public function getConfigApprovedOnly(Election $election, $default_value = true) { + return $election->get_config_value('approved_only', $default_value); + } + + public function voterDetails(Election $election) { + $electionId = $election->getKey(); + + $stats = array( + "outstanding_invites" => array(), + "approved_none" => array(), + "approved_current" => array(), + "approved_previous" => array() + ); + + $query = Election::query() + // Convert to base so Election models are not returned. + ->toBase() + ->where('elections.id', '=', $electionId) + ->join('electors', 'elections.id', '=', 'electors.election_id') + ->leftJoin('users', 'electors.user_id', '=', 'users.id') + ->select('users.name', + 'users.email', + 'electors.id', + 'electors.voter_name', + 'electors.invite_email', + 'electors.invite_accepted_at', + 'elections.ballot_version', + 'electors.ballot_version_approved'); + + foreach ($query->get() as $row) { + if ($row->invite_accepted_at == null) { + $key = 'outstanding_invites'; + } else if ($row->ballot_version_approved == null) { + $key = 'approved_none'; + } else if ($row->ballot_version_approved == $row->ballot_version) { + $key = 'approved_current'; + } else { + $key = 'approved_previous'; + } + + $user_name = $row->name; + $voter_name = $row->voter_name; + $email = $row->email != null ? $row->email : $row->invite_email; + $elector_id = $row->id; + # user_name may be different from voter_name if this is a proxy-voting use case. + # user_name may be null if the elector hasn't created an + # account yet. voter_name will be non null iff user is + # proxy voting on behalf of voter. + $elector = [ + "user_name" => $user_name, + "voter_name" => $voter_name, + "email" => $email, + "elector_id" => $elector_id + ]; + $stats[$key][] = $elector; + } + + return $stats; + } +} \ No newline at end of file diff --git a/app/Actions/GetVoterDetails.php b/app/Actions/GetVoterDetails.php new file mode 100644 index 0000000..7f82926 --- /dev/null +++ b/app/Actions/GetVoterDetails.php @@ -0,0 +1,68 @@ +getKey(); + + $stats = [ + "outstanding_invites" => [], + "approved_none" => [], + "approved_current" => [], + "approved_previous" => [] + ]; + + $query = Election::query() + // Convert to base so Election models are not returned. + ->toBase() + ->where('elections.id', '=', $electionId) + ->join('electors', 'elections.id', '=', 'electors.election_id') + ->leftJoin('users', 'electors.user_id', '=', 'users.id') + ->select('users.name', + 'users.email', + 'electors.id', + 'electors.voter_name', + 'electors.invite_email', + 'electors.invite_accepted_at', + 'elections.ballot_version', + 'electors.ballot_version_approved'); + + foreach ($query->get() as $row) { + if ($row->invite_accepted_at == null) { + $key = 'outstanding_invites'; + } else if ($row->ballot_version_approved == null) { + $key = 'approved_none'; + } else if ($row->ballot_version_approved == $row->ballot_version) { + $key = 'approved_current'; + } else { + $key = 'approved_previous'; + } + + $user_name = $row->name; + $voter_name = $row->voter_name; + $email = $row->email != null ? $row->email : $row->invite_email; + $elector_id = $row->id; + # user_name may be different from voter_name if this is a proxy-voting use case. + # user_name may be null if the elector hasn't created an + # account yet. voter_name will be non null iff user is + # proxy voting on behalf of voter. + $elector = [ + "user_name" => $user_name, + "voter_name" => $voter_name, + "email" => $email, + "elector_id" => $elector_id + ]; + $stats[$key][] = $elector; + } + + return $stats; + } + +} \ No newline at end of file diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index b725cc5..a3f1fc3 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -9,6 +9,8 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Throwable; class Handler extends ExceptionHandler @@ -16,7 +18,7 @@ class Handler extends ExceptionHandler /** * A list of the exception types that should not be reported. * - * @var array + * @var array> */ protected $dontReport = [ AuthenticationException::class, @@ -42,9 +44,8 @@ class Handler extends ExceptionHandler * Render an exception into an HTTP response. * * @param Request $request - * @return Response */ - public function render($request, Throwable $e) + public function render($request, Throwable $e): Response|JsonResponse|SymfonyResponse { if ($request->expectsJson()) { @@ -65,7 +66,7 @@ public function render($request, Throwable $e) $status = 400; // If this exception is an instance of HttpException - if ($this->isHttpException($e)) { + if ($e instanceof HttpExceptionInterface) { // Grab the HTTP status code from the Exception $status = $e->getStatusCode(); } elseif ($e instanceof ModelNotFoundException) { diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 8fd55cb..3103656 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -33,8 +33,6 @@ class RegisterController extends Controller /** * Create a new controller instance. - * - * @return void */ public function __construct() { @@ -43,8 +41,6 @@ public function __construct() /** * Show the application registration form. - * - * @return \Illuminate\Http\Response */ public function showRegistrationForm(Request $request) { @@ -75,9 +71,6 @@ protected function verify_token(array $data) /** * Get a validator for an incoming registration request. - * - * @param array $data - * @return \Illuminate\Contracts\Validation\Validator */ protected function validator(array $data) { @@ -90,9 +83,6 @@ protected function validator(array $data) /** * Create a new user instance after a valid registration. - * - * @param array $data - * @return User */ protected function create(array $data) { diff --git a/app/Http/Controllers/CandidateController.php b/app/Http/Controllers/CandidateController.php index 1093057..a0dd2d0 100644 --- a/app/Http/Controllers/CandidateController.php +++ b/app/Http/Controllers/CandidateController.php @@ -32,8 +32,6 @@ class CandidateController extends Controller * )), * @OA\Response(response="400", description="Bad Request") * ) - * - * @return \Illuminate\Http\Response */ public function index(Election $election) { @@ -68,10 +66,6 @@ public function index(Election $election) * ), * @OA\Response(response="400", description="Bad Request") * ) - * - * @param Election $election - * @param Candidate $candidate - * @return Candidate */ public function show(Election $election, Candidate $candidate) { @@ -112,10 +106,6 @@ public function show(Election $election, Candidate $candidate) * description="Bad Request", * ) * ) - * - * @param Request $request - * @param Election $election - * @return \Illuminate\Http\RedirectResponse */ public function store(Request $request, Election $election) { @@ -169,10 +159,6 @@ public function store(Request $request, Election $election) * description="Bad Request", * ) * ) - * - * @param Election $election - * @param Candidate $candidate - * @return \Illuminate\Http\JsonResponse */ public function update(Request $request, Election $election, Candidate $candidate) { @@ -224,10 +210,6 @@ public function update(Request $request, Election $election, Candidate $candidat * description="Bad Request", * ) * ) - * - * @param Election $election - * @param Candidate $candidate - * @return \Illuminate\Http\JsonResponse */ public function destroy(Election $election, Candidate $candidate) { diff --git a/app/Http/Controllers/ElectionController.php b/app/Http/Controllers/ElectionController.php index 71989f4..db20416 100644 --- a/app/Http/Controllers/ElectionController.php +++ b/app/Http/Controllers/ElectionController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Actions\GetVoterDetails; use App\Models\Candidate; use App\Models\CandidateRank; use App\Models\Election; @@ -65,9 +66,6 @@ public function index() /** * Store a newly created resource in storage. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response */ public function store(Request $request) { @@ -103,9 +101,6 @@ public function store(Request $request) * )), * @OA\Response(response="400", description="Bad Request") * ) - * - * @param \App\Models\Election $election - * @return \Illuminate\Http\Response */ public function show(Election $election) { @@ -116,10 +111,6 @@ public function show(Election $election) /** * Update the specified resource in storage. - * - * @param \Illuminate\Http\Request $request - * @param \App\Models\Election $election - * @return \Illuminate\Http\Response */ public function update(Request $request, Election $election) { @@ -140,9 +131,6 @@ public function update(Request $request, Election $election) /** * Remove the specified resource from storage. - * - * @param \App\Models\Election $election - * @return \Illuminate\Http\Response */ public function destroy(Election $election) { @@ -261,21 +249,23 @@ public function voter_stats(Request $request, $election_id) $election = Election::where('id', '=', $election_id)->firstOrFail(); $this->authorize('view_voter_stats', $election); - $stats = array( + $stats = [ "outstanding_invites" => 0, "approved_none" => 0, "approved_current" => 0, "approved_previous" => 0 - ); + ]; $columns = DB::raw('count(*) AS elector_count, elections.ballot_version, electors.ballot_version_approved, (electors.invite_accepted_at IS NOT NULL) AS accepted'); - $query = Election::where('elections.id', '=', $election_id) + $query = Election::query() + ->toBase() + ->where('elections.id', '=', $election_id) ->join('electors', 'elections.id', '=', 'electors.election_id') ->select($columns) ->groupBy('elections.ballot_version', 'electors.ballot_version_approved', 'accepted'); foreach ($query->get() as $row) { - $count = $row['elector_count']; + $count = $row->elector_count; if (!$row->accepted) { $stats['outstanding_invites'] += $count; @@ -291,11 +281,11 @@ public function voter_stats(Request $request, $election_id) return response()->json($stats); } - public function voter_details(Request $request, $election_id) + public function voter_details(Request $request, $election_id, GetVoterDetails $getVoterDetails) { $election = Election::where('id', '=', $election_id)->firstOrFail(); $this->authorize('view_voter_details', $election); - $stats = $election->voter_details(); + $stats = $getVoterDetails($election); return response()->json($stats); } diff --git a/app/Http/Controllers/ElectorController.php b/app/Http/Controllers/ElectorController.php index 7d182a9..dabb95a 100644 --- a/app/Http/Controllers/ElectorController.php +++ b/app/Http/Controllers/ElectorController.php @@ -5,7 +5,6 @@ use App\Models\Election; use App\Models\Elector; use Carbon\Carbon; -use DummyFullModelClass; use Illuminate\Support\Facades\Auth; class ElectorController extends Controller @@ -31,9 +30,6 @@ class ElectorController extends Controller * )), * @OA\Response(response="400", description="Bad Request") * ) - * - * @param \App\Models\Election $election - * @return \Illuminate\Http\Response */ public function index(Election $election) { @@ -88,9 +84,6 @@ public function electors_for_self(Election $election) * ), * @OA\Response(response="400", description="Bad Request") * ) - * - * @param \App\Models\Election $election - * @return \Illuminate\Http\Response */ public function show(Election $election, $elector_id) { @@ -102,10 +95,6 @@ public function show(Election $election, $elector_id) /** * Remove the specified resource from storage. - * - * @param \App\Models\Election $election - * @param \App\Models\User $user - * @return \Illuminate\Http\Response */ public function destroy(Election $election, $elector_id) { diff --git a/app/Http/Controllers/InviteController.php b/app/Http/Controllers/InviteController.php index 0dbd898..84c6a36 100644 --- a/app/Http/Controllers/InviteController.php +++ b/app/Http/Controllers/InviteController.php @@ -34,8 +34,6 @@ class InviteController extends Controller * )), * @OA\Response(response="400", description="Bad Request") * ) - * - * @return \Illuminate\Http\Response */ public function index(Election $election) { @@ -119,9 +117,6 @@ public function store( * description="Bad Request", * ) * ) - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response */ public function accept(Request $request) { diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 88ca93b..3dd2c10 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -10,8 +10,6 @@ class ProfileController extends Controller { /** * Show the application dashboard. - * - * @return \Illuminate\Http\Response */ public function index() { @@ -20,8 +18,6 @@ public function index() /** * Show the invite accept form. - * - * @return \Illuminate\Http\Response */ public function accept(Request $request) { @@ -29,7 +25,7 @@ public function accept(Request $request) $user = Auth::user(); if (empty($user)) { - Session::set('invite', $invite); + Session::push('invite', $invite); return redirect()->route('login'); } @@ -37,9 +33,7 @@ public function accept(Request $request) } /** - * Greet user who created new account. - * - * @return \Illuminate\Http\Response + * Greet user who created a new account. */ public function new_account() { diff --git a/app/Http/Controllers/ResultController.php b/app/Http/Controllers/ResultController.php index e733972..8755b4d 100644 --- a/app/Http/Controllers/ResultController.php +++ b/app/Http/Controllers/ResultController.php @@ -1,6 +1,7 @@ authorize('view_results', $election); - return $election->calculateResult(); + return $calculateResult($election); } } diff --git a/app/Http/Controllers/ResultSnapshotController.php b/app/Http/Controllers/ResultSnapshotController.php index 7cef920..0e7c13e 100644 --- a/app/Http/Controllers/ResultSnapshotController.php +++ b/app/Http/Controllers/ResultSnapshotController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Actions\CalculateResult; use App\Models\Election; use App\Models\ResultSnapshot; use Illuminate\Http\Request; @@ -37,11 +38,11 @@ public function show(Election $election, $snapshot_id) return $snapshot; } - public function store(Request $request, Election $election) + public function store(Request $request, Election $election, CalculateResult $calculateResult) { $this->authorize('update', $election); - $result = $election->calculateResult(); + $result = $calculateResult($election); # snapshot result $snapshot = new ResultSnapshot(); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index f5ebc25..84c6a78 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -10,8 +10,6 @@ class Kernel extends HttpKernel * The application's global HTTP middleware stack. * * These middlewares are run during every request to your application. - * - * @var array */ protected $middleware = [ // \App\Http\Middleware\TrustHosts::class, @@ -25,8 +23,6 @@ class Kernel extends HttpKernel /** * The application's route middleware groups. - * - * @var array> */ protected $middlewareGroups = [ 'web' => [ @@ -50,8 +46,6 @@ class Kernel extends HttpKernel * The application's route middleware. * * These middlewares may be assigned to groups or used individually. - * - * @var array */ protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, diff --git a/app/Http/Middleware/ParseLaravelToken.php b/app/Http/Middleware/ParseLaravelToken.php deleted file mode 100644 index ec169bb..0000000 --- a/app/Http/Middleware/ParseLaravelToken.php +++ /dev/null @@ -1,36 +0,0 @@ -encrypter = $encrypter; - } - - public function handle($request, Closure $next, $guard = null) - { - $encryptedToken = $request->cookie('laravel_token'); - - if (!empty($encryptedToken)) { - $key = $this->encrypter->getKey(); - $token = $this->encrypter->decrypt($encryptedToken); - $data = JWT::decode($token, $key, ['HS256']); - if (false === $request->headers->get('x-csrf-token', false)) { - $request->headers->add([ - 'X-CSRF-TOKEN' => $data->csrf, - 'X-Requested-With' => 'XMLHttpRequest', - 'X-XSRF-TOKEN' => $request->cookie('laravel_token'), - ]); - } - } - - return $next($request); - } -} diff --git a/app/Models/Election.php b/app/Models/Election.php index 74843f3..919a357 100644 --- a/app/Models/Election.php +++ b/app/Models/Election.php @@ -7,7 +7,9 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Notification; use PivotLibre\Tideman\Ballot; @@ -17,7 +19,7 @@ use PivotLibre\Tideman\RankedPairsCalculator; /** - * @property Collection electors + * @property Collection $electors * @property string $name */ class Election extends Model @@ -27,8 +29,6 @@ class Election extends Model /** * The attributes that are mass assignable. - * - * @var array */ protected $fillable = ['name']; @@ -38,8 +38,6 @@ class Election extends Model /** * The attributes that should be mutated to dates. - * - * @var array */ protected $dates = ['deleted_at']; @@ -48,74 +46,17 @@ public function creator(): BelongsTo return $this->belongsTo(User::class, 'creator_id'); } - public function voter_details() { - $electionId = $this->getKey(); - - $stats = array( - "outstanding_invites" => array(), - "approved_none" => array(), - "approved_current" => array(), - "approved_previous" => array() - ); - - $query = Election::where('elections.id', '=', $electionId) - ->join('electors', 'elections.id', '=', 'electors.election_id') - ->leftJoin('users', 'electors.user_id', '=', 'users.id') - ->select('users.name', - 'users.email', - 'electors.id', - 'electors.voter_name', - 'electors.invite_email', - 'electors.invite_accepted_at', - 'elections.ballot_version', - 'electors.ballot_version_approved'); - - foreach ($query->get() as $row) { - $key = null; - if ($row->invite_accepted_at == null) { - $key = 'outstanding_invites'; - } else if ($row->ballot_version_approved == null) { - $key = 'approved_none'; - } else if ($row->ballot_version_approved == $row->ballot_version) { - $key = 'approved_current'; - } else { - $key = 'approved_previous'; - } - - $user_name = $row->name; - $voter_name = $row->voter_name; - $email = $row->email != null ? $row->email : $row->invite_email; - $elector_id = $row->id; - # user_name may be different from voter_name if this is a proxy-voting use case. - # user_name may be null if the elector hasn't created an - # account yet. voter_name will be non null iff user is - # proxy voting on behalf of voter. - $elector = array( - "user_name" => $user_name, - "voter_name" => $voter_name, - "email" => $email, - "elector_id" => $elector_id - ); - array_push($stats[$key], $elector); - } - - return $stats; - } - - /** - * @return \Illuminate\Database\Eloquent\Relations\HasMany|Elector - */ - public function electors() + public function electors(): HasMany { return $this->hasMany(Elector::class); } - public function candidates() + public function candidates(): HasMany { return $this->hasMany(Candidate::class); } - public function result_snapshots() + public function result_snapshots(): HasMany { return $this->hasMany(ResultSnapshot::class); } @@ -127,222 +68,4 @@ public function get_config_value($key, $default_value) { } return $config[$key]; } - - public function get_config_approved_only($default_value=true) { - return $this->get_config_value('approved_only', $default_value); - } - - /** - * @param int $electionId - * @return Collection of all CandidateRanks associated with - * the election identified by the parameterized id. - */ - public function getCandidateRankCollection() - { - $electionId = $this->getKey(); - $query = \DB::table('elections')->where('elections.id', '=', $electionId) - ->join('candidates', 'candidates.election_id', '=', 'elections.id') - ->join('electors', 'electors.election_id', '=', 'elections.id') - ->leftJoin('candidate_ranks', function($join) { - $join->on('candidate_ranks.elector_id', '=', 'electors.id'); - $join->on('candidate_ranks.candidate_id', '=', 'candidates.id'); - }); - if ($this->get_config_approved_only()) { - $query = $query->where('electors.ballot_version_approved', '=', \DB::raw('elections.ballot_version')); - } - $query = $query->select('electors.id AS elector_id', 'candidates.id AS candidate_id', 'candidates.name', 'candidate_ranks.rank'); - - Log::debug('BALLOT QUERY: ' . $query->toSql()); - - return $query->get(); - } - - /** - * @param Collection of App\CandidateRank - * @return array of arrays of arrays. The arrays are grouped at the outermost level - * on elector id. The arrays are grouped at the next level by rank. Array entries at this level are ordered in - * ascending rank. The innermost arrays are associative arrays that contain CandidateRank attributes. - */ - public function groupRankingsByElectorAndRank($candidateRanks) - { - // determine largest (worst) rank - $max_rank = 1; - $ranks = array_map(function($candidateRank){return $candidateRank->rank;}, $candidateRanks->toArray()); - if (count($ranks) > 0) { - $max_rank = max($ranks); - } - $unranked = $max_rank + 1; - - // bucketize by elector - $candidateRanksGroupedByElector = $candidateRanks->mapToGroups(function($candidateRank){ - $key = $candidateRank->elector_id; - $value = $candidateRank; - return [ $key => $value ]; - }); - - // bucketize by rank within each elector - $candidateRanksGroupedByElectorAndRank = $candidateRanksGroupedByElector->map(function($candidateRanksFromOneElector) use ($unranked) { - return $candidateRanksFromOneElector->mapToGroups(function($candidateRank) use ($unranked) { - // default rank is <= 0. Map that to largest value - $key = (is_null($candidateRank->rank) || $candidateRank->rank <= 0) ? $unranked : $candidateRank->rank; - $value = $candidateRank; - return [ $key => $value ]; - }); - })->toArray(); - return $candidateRanksGroupedByElectorAndRank; - } - - /** - * @param array of arrays of arrays as output by $this->groupRankingsByElectorAndRank - * @return array of NBallots - */ - public function buildNBallots() - { - $electionId = $this->getKey(); - $candidateRanks = $this->getCandidateRankCollection(); - $candidateRanksGroupedByElectorAndRank = $this->groupRankingsByElectorAndRank($candidateRanks); - - $nBallots = array_map(function($ballotArray){ - $candidateLists = array_map(function($candidateListArray){ - - $candidates = array_map(function($candidateArray){ - return new TidemanCandidate($candidateArray->candidate_id, $candidateArray->name); - }, $candidateListArray); - - $candidateList = new CandidateList(...$candidates); - return $candidateList; - }, $ballotArray); - - # sort by rank (best=1 first) - ksort($candidateLists); - array_reverse($candidateLists); - - $nBallot = new NBallot(1, ...$candidateLists); - return $nBallot; - }, $candidateRanksGroupedByElectorAndRank); - return $nBallots; - } - - public function createTotallyOrderedBallot($nBallot) { - $totalOrder = array(); - foreach ($nBallot as $candidateList) { - $candidates = (clone $candidateList)->toArray(); - shuffle($candidates); - array_push($totalOrder, ...$candidates); - } - - // every candidate should be in its own CandidateList - $totalOrder = array_map(function($candidate){ - return new CandidateList($candidate); - }, $totalOrder); - - return new Ballot(...$totalOrder); - } - - public static function ballotToText($nBallot) { - $line = implode(">", - array_map(function($candidateList) { - return implode("=", - array_map(function($candidate) { - return $candidate->getId(); - }, $candidateList->toArray())); - }, $nBallot->toArray())); - return $line; - } - - public static function ballotsToText($nBallots) { - $lines = []; - foreach ($nBallots as $nBallot) { - array_push($lines, self::ballotToText($nBallot)); - } - return implode("\n", $lines); - } - - public function calculateResult() - { - // values to populate for the result snapshot - $pivotCandidates = null; - $pivotElectors = null; - $nBallots = null; - $tieBreaker = null; - $tieBreakerTotal = null; - $pivotWinners = null; - $errorMessage = null; - $exceptionMessage = null; - $exceptionStack = null; - - # calculate Tideman - try { - # generate Tideman inputs - $pivotElectors = $this->voter_details(); - $nBallots = $this->buildNBallots(); - Log::debug('BALLOTS: ' . self::ballotsToText($nBallots)); - if (count($nBallots) == 0) { - $errorMessage = "there were 0 ballots ready for the election"; - throw new \Exception($errorMessage); - } - $tieBreaker = $nBallots[array_rand($nBallots)]; - $tieBreakerTotal = self::createTotallyOrderedBallot($tieBreaker); - - // calculated results - $calculator = new RankedPairsCalculator($tieBreakerTotal); - $numWinners = $this->candidates()->count(); - $result = $calculator->calculate($numWinners, null, ...$nBallots); - $tidemanWinners = $result->getRanking(); - - // translate tideman candidate objects back to pivot candidate objects - $pivotCandidates = $this->candidates()->get()->keyBy('id'); - $pivotWinners = array_map(function($tidemanCandidateList) use ($pivotCandidates){ - return array_map(function($tidemanCandidate) use ($pivotCandidates){ - $candidateId = $tidemanCandidate->getId(); - return $pivotCandidates[$candidateId]; - }, $tidemanCandidateList->toArray()); - }, $tidemanWinners->toArray()); - } catch (\Exception $e) { - // visible to users - if (is_null($errorMessage)) { - $errorMessage = 'a result could not be computed for this snapshot'; - } - - // for debug blob - $exceptionMessage = $e->getMessage(); - $exceptionStack = $e->getTraceAsString(); - } - - // populate snapshot blob (json): debug, debug_private, and order - $debug = array(); - $debug["ballots"] = is_null($nBallots) ? null : array_map('self::ballotToText', $nBallots); - $debug["tie_breaker"] = is_null($tieBreaker) ? null : self::ballotToText($tieBreaker); - $debug["tie_breaker_total"] = is_null($tieBreakerTotal) ? null : self::ballotToText($tieBreakerTotal); - $debug["election_config"] = $this->config; - $debug["exception"] = array("message" => $exceptionMessage, "stack" => $exceptionStack); - $result = array(); - $debug_private = array(); - $debug_private["candidates"] = array(); - if (!is_null($pivotCandidates)) { - foreach ($pivotCandidates as $c) { - array_push($debug_private["candidates"], array("id"=>$c->id, "name"=>$c->name)); - } - } - $debug_private["electors"] = array(); - if (!is_null($pivotElectors)) { - foreach ($pivotElectors as $state => $electors) { - foreach ($electors as $e) { - $row = array("id"=>$e["elector_id"], - "name"=>$e["voter_name"] ? $e["voter_name"] : $e["user_name"], - "email"=>$e["email"], - "state"=>$state); - array_push($debug_private["electors"], $row); - } - } - } - - // top-level fields in snapshot blob - $result["error"] = $errorMessage; - $result["debug"] = $debug; - $result["debug_private"] = $debug_private; - $result["order"] = $pivotWinners; - - return $result; - } } diff --git a/app/Models/User.php b/app/Models/User.php index 380a46f..0dabced 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -16,8 +16,6 @@ class User extends Authenticatable /** * The attributes that are mass assignable. - * - * @var array */ protected $fillable = [ 'name', 'email', 'password', @@ -25,8 +23,6 @@ class User extends Authenticatable /** * The attributes that should be hidden for arrays. - * - * @var array */ protected $hidden = [ 'password', 'remember_token', diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 9d2c678..e9202cf 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -12,7 +12,7 @@ class AuthServiceProvider extends ServiceProvider /** * The policy mappings for the application. * - * @var array + * @var array */ protected $policies = [ Election::class => ElectionPolicy::class, @@ -20,10 +20,8 @@ class AuthServiceProvider extends ServiceProvider /** * Register any authentication / authorization services. - * - * @return void */ - public function boot() + public function boot(): void { $this->registerPolicies(); diff --git a/composer.json b/composer.json index 65271e9..a48f130 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "fakerphp/faker": "^1.19", "mockery/mockery": "^1.5", "nunomaduro/collision": "^6.1", + "nunomaduro/larastan": "^2.1", "phpunit/phpunit": "^9.5", "spatie/laravel-ignition": "^1.0" }, @@ -60,7 +61,8 @@ "post-update-cmd": [ "Illuminate\\Foundation\\ComposerScripts::postUpdate", "php artisan optimize" - ] + ], + "phpstan": "vendor/bin/phpstan analyse" }, "config": { "preferred-install": "dist", diff --git a/composer.lock b/composer.lock index 294e819..7a964af 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "00a61fee1d37d46138a56535de046858", + "content-hash": "39f083a7352b6ab2619f8481e46912ef", "packages": [ { "name": "aws/aws-crt-php", @@ -58,16 +58,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.227.1", + "version": "3.228.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "dd3271b171cd83dfcfa3cfc390057114d8f010b1" + "reference": "4ff51d01da43aa3bd36eef921a9cd4e0ff843fab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/dd3271b171cd83dfcfa3cfc390057114d8f010b1", - "reference": "dd3271b171cd83dfcfa3cfc390057114d8f010b1", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4ff51d01da43aa3bd36eef921a9cd4e0ff843fab", + "reference": "4ff51d01da43aa3bd36eef921a9cd4e0ff843fab", "shasum": "" }, "require": { @@ -143,9 +143,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.227.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.228.0" }, - "time": "2022-06-20T18:14:17+00:00" + "time": "2022-06-21T18:13:25+00:00" }, { "name": "brick/math", @@ -1418,16 +1418,16 @@ }, { "name": "laravel/framework", - "version": "v9.17.0", + "version": "v9.18.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "091e287678ac723c591509ca6374e4ded4a99b1c" + "reference": "93a1296bca43c1ca8dcb5df8f97107e819a71499" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/091e287678ac723c591509ca6374e4ded4a99b1c", - "reference": "091e287678ac723c591509ca6374e4ded4a99b1c", + "url": "https://api.github.com/repos/laravel/framework/zipball/93a1296bca43c1ca8dcb5df8f97107e819a71499", + "reference": "93a1296bca43c1ca8dcb5df8f97107e819a71499", "shasum": "" }, "require": { @@ -1439,7 +1439,7 @@ "fruitcake/php-cors": "^1.2", "laravel/serializable-closure": "^1.0", "league/commonmark": "^2.2", - "league/flysystem": "^3.0", + "league/flysystem": "^3.0.16", "monolog/monolog": "^2.0", "nesbot/carbon": "^2.53.1", "php": "^8.0.2", @@ -1515,7 +1515,7 @@ "pda/pheanstalk": "^4.0", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^9.5.8", - "predis/predis": "^1.1.9", + "predis/predis": "^1.1.9|^2.0", "symfony/cache": "^6.0" }, "suggest": { @@ -1541,7 +1541,7 @@ "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", "phpunit/phpunit": "Required to use assertions and run tests (^9.5.8).", - "predis/predis": "Required to use the predis connector (^1.1.9).", + "predis/predis": "Required to use the predis connector (^1.1.9|^2.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", "symfony/cache": "Required to PSR-6 cache bridge (^6.0).", @@ -1593,7 +1593,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-06-07T15:09:32+00:00" + "time": "2022-06-21T14:40:11+00:00" }, { "name": "laravel/passport", @@ -3107,16 +3107,16 @@ }, { "name": "nyholm/psr7", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "1461e07a0f2a975a52082ca3b769ca912b816226" + "reference": "f734364e38a876a23be4d906a2a089e1315be18a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/1461e07a0f2a975a52082ca3b769ca912b816226", - "reference": "1461e07a0f2a975a52082ca3b769ca912b816226", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/f734364e38a876a23be4d906a2a089e1315be18a", + "reference": "f734364e38a876a23be4d906a2a089e1315be18a", "shasum": "" }, "require": { @@ -3168,7 +3168,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.5.0" + "source": "https://github.com/Nyholm/psr7/tree/1.5.1" }, "funding": [ { @@ -3180,20 +3180,20 @@ "type": "github" } ], - "time": "2022-02-02T18:37:57+00:00" + "time": "2022-06-22T07:13:36+00:00" }, { "name": "paragonie/constant_time_encoding", - "version": "v2.6.2", + "version": "v2.6.3", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "c1b1d82d109846ba58a4664dc5480c69ad2fc097" + "reference": "58c3f47f650c94ec05a151692652a868995d2938" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/c1b1d82d109846ba58a4664dc5480c69ad2fc097", - "reference": "c1b1d82d109846ba58a4664dc5480c69ad2fc097", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", "shasum": "" }, "require": { @@ -3247,7 +3247,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2022-06-13T05:29:16+00:00" + "time": "2022-06-14T06:56:20+00:00" }, { "name": "paragonie/random_compat", @@ -7324,6 +7324,77 @@ ], "time": "2022-03-28T07:55:11+00:00" }, + { + "name": "composer/pcre", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/e300eb6c535192decd27a85bc72a9290f0d6b3bd", + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.0.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T20:21:48+00:00" + }, { "name": "doctrine/instantiator", "version": "1.4.1", @@ -7854,6 +7925,104 @@ ], "time": "2022-04-05T15:31:38+00:00" }, + { + "name": "nunomaduro/larastan", + "version": "v2.1.11", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/larastan.git", + "reference": "8514c5ec475b440702f08cf804e73cd55a05f622" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/8514c5ec475b440702f08cf804e73cd55a05f622", + "reference": "8514c5ec475b440702f08cf804e73cd55a05f622", + "shasum": "" + }, + "require": { + "composer/pcre": "^3.0", + "ext-json": "*", + "illuminate/console": "^9", + "illuminate/container": "^9", + "illuminate/contracts": "^9", + "illuminate/database": "^9", + "illuminate/http": "^9", + "illuminate/pipeline": "^9", + "illuminate/support": "^9", + "mockery/mockery": "^1.4.4", + "php": "^8.0.2", + "phpmyadmin/sql-parser": "^5.5", + "phpstan/phpstan": "^1.7.12" + }, + "require-dev": { + "nikic/php-parser": "^4.13.2", + "orchestra/testbench": "^7.0.0", + "phpunit/phpunit": "^9.5.11" + }, + "suggest": { + "orchestra/testbench": "Using Larastan for analysing a package needs Testbench" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "NunoMaduro\\Larastan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan wrapper for Laravel", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "larastan", + "laravel", + "package", + "php", + "static analysis" + ], + "support": { + "issues": "https://github.com/nunomaduro/larastan/issues", + "source": "https://github.com/nunomaduro/larastan/tree/v2.1.11" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/canvural", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2022-06-13T21:45:42+00:00" + }, { "name": "phar-io/manifest", "version": "2.0.3", @@ -8125,6 +8294,79 @@ }, "time": "2022-03-15T21:29:03+00:00" }, + { + "name": "phpmyadmin/sql-parser", + "version": "5.5.0", + "source": { + "type": "git", + "url": "https://github.com/phpmyadmin/sql-parser.git", + "reference": "8ab99cd0007d880f49f5aa1807033dbfa21b1cb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/8ab99cd0007d880f49f5aa1807033dbfa21b1cb5", + "reference": "8ab99cd0007d880f49f5aa1807033dbfa21b1cb5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "symfony/polyfill-mbstring": "^1.3" + }, + "conflict": { + "phpmyadmin/motranslator": "<3.0" + }, + "require-dev": { + "phpmyadmin/coding-standard": "^3.0", + "phpmyadmin/motranslator": "^4.0 || ^5.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.2", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/php-code-coverage": "*", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "^0.16.1", + "vimeo/psalm": "^4.11", + "zumba/json-serializer": "^3.0" + }, + "suggest": { + "ext-mbstring": "For best performance", + "phpmyadmin/motranslator": "Translate messages to your favorite locale" + }, + "bin": [ + "bin/highlight-query", + "bin/lint-query", + "bin/tokenize-query" + ], + "type": "library", + "autoload": { + "psr-4": { + "PhpMyAdmin\\SqlParser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "The phpMyAdmin Team", + "email": "developers@phpmyadmin.net", + "homepage": "https://www.phpmyadmin.net/team/" + } + ], + "description": "A validating SQL lexer and parser with a focus on MySQL dialect.", + "homepage": "https://github.com/phpmyadmin/sql-parser", + "keywords": [ + "analysis", + "lexer", + "parser", + "sql" + ], + "support": { + "issues": "https://github.com/phpmyadmin/sql-parser/issues", + "source": "https://github.com/phpmyadmin/sql-parser" + }, + "time": "2021-12-09T04:31:52+00:00" + }, { "name": "phpspec/prophecy", "version": "v1.15.0", @@ -8192,6 +8434,65 @@ }, "time": "2021-12-08T12:19:24+00:00" }, + { + "name": "phpstan/phpstan", + "version": "1.7.15", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "cd0202ea1b1fc6d1bbe156c6e2e18a03e0ff160a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd0202ea1b1fc6d1bbe156c6e2e18a03e0ff160a", + "reference": "cd0202ea1b1fc6d1bbe156c6e2e18a03e0ff160a", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "source": "https://github.com/phpstan/phpstan/tree/1.7.15" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpstan", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2022-06-20T08:29:01+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "9.2.15", @@ -8512,16 +8813,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.20", + "version": "9.5.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba" + "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/12bc8879fb65aef2138b26fc633cb1e3620cffba", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1", + "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1", "shasum": "" }, "require": { @@ -8555,7 +8856,6 @@ "sebastian/version": "^3.0.2" }, "require-dev": { - "ext-pdo": "*", "phpspec/prophecy-phpunit": "^2.0.1" }, "suggest": { @@ -8599,7 +8899,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.21" }, "funding": [ { @@ -8611,7 +8911,7 @@ "type": "github" } ], - "time": "2022-04-01T12:37:26+00:00" + "time": "2022-06-19T12:14:25+00:00" }, { "name": "sebastian/cli-parser", diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..14a2680 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,9 @@ +includes: + - ./vendor/nunomaduro/larastan/extension.neon + +parameters: + paths: + - app + + # Level 9 is the highest level + level: 5 \ No newline at end of file diff --git a/tests/Feature/CandidateTest.php b/tests/Feature/CandidateTest.php index 0e14cba..1c726d4 100644 --- a/tests/Feature/CandidateTest.php +++ b/tests/Feature/CandidateTest.php @@ -12,15 +12,12 @@ use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; use Illuminate\Database\Eloquent\ModelNotFoundException; -use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Validation\ValidationException; use Laravel\Passport\Passport; use Tests\TestCase; class CandidateTest extends TestCase { - use RefreshDatabase; - /** @test */ public function cannot_get_a_candidate_if_not_creator_or_elector() { @@ -125,7 +122,6 @@ public function cannot_get_all_candidates_if_guest() { $election = Election::factory()->create(); - $response = $this->getJson("api/elections/{$election->id}/candidates"); $response->assertStatus(400); diff --git a/tests/Feature/ElectionTest.php b/tests/Feature/ElectionTest.php index 1536cc7..ccfb6ea 100644 --- a/tests/Feature/ElectionTest.php +++ b/tests/Feature/ElectionTest.php @@ -10,15 +10,11 @@ use App\Models\User; use Illuminate\Auth\AuthenticationException; use Illuminate\Database\Eloquent\ModelNotFoundException; -use Illuminate\Foundation\Testing\DatabaseTransactions; -use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Passport\Passport; use Tests\TestCase; class ElectionTest extends TestCase { - use RefreshDatabase; - /** @test */ public function can_get_a_election() { diff --git a/tests/Feature/ElectorTest.php b/tests/Feature/ElectorTest.php index 52464e4..ae9742d 100644 --- a/tests/Feature/ElectorTest.php +++ b/tests/Feature/ElectorTest.php @@ -9,14 +9,11 @@ use App\Models\User; use Carbon\Carbon; use Illuminate\Database\Eloquent\ModelNotFoundException; -use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Passport\Passport; use Tests\TestCase; class ElectorTest extends TestCase { - use RefreshDatabase; - /** @test */ public function can_get_electors() { diff --git a/tests/TestCase.php b/tests/TestCase.php index 5137b47..d20aeb6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Database\Eloquent\Model; +use Illuminate\Foundation\Testing\LazilyRefreshDatabase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use App\Exceptions\Handler; use Illuminate\Support\Collection; @@ -11,6 +12,7 @@ abstract class TestCase extends BaseTestCase { use CreatesApplication; + use LazilyRefreshDatabase; protected function assertContainsAllModels(Collection $models, Collection $responseModel) {