From 84a22e7f9d60953f73375eb13f990fab176e3ac3 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Mon, 22 Aug 2022 20:32:14 -0500 Subject: [PATCH 1/3] Adapt the unit test action to not use a docker image. This uses Ubuntu 22.04 for the runner and docker image. --- .github/workflows/linter.yml | 2 +- .github/workflows/unit-tests.yml | 81 +++++++++++++++++++++----- docker/webwork3.dockerfile | 73 ++++++++++++++--------- lib/DB/Schema/Result/Attempt.pm | 2 +- lib/DB/Schema/Result/CourseSettings.pm | 2 +- lib/DB/Schema/Result/CourseUser.pm | 2 +- lib/DB/Schema/Result/PoolProblem.pm | 2 +- lib/DB/Schema/Result/ProblemSet.pm | 4 +- lib/DB/Schema/Result/SetProblem.pm | 2 +- lib/DB/Schema/Result/UserProblem.pm | 2 +- lib/DB/Schema/Result/UserSet.pm | 2 +- t/db/001_courses.t | 3 +- t/db/004_course_users.t | 4 +- t/db/006_quizzes.t | 10 +++- t/mojolicious/001_login.t | 11 ++-- t/mojolicious/002_courses.t | 12 ++-- t/mojolicious/003_users.t | 14 ++--- t/mojolicious/008_problems.t | 18 +++--- t/mojolicious/009_user_problems.t | 10 ++-- t/mojolicious/010_problem_pools.t | 4 +- 20 files changed, 165 insertions(+), 95 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 0b2e6f24..559abec1 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -56,7 +56,7 @@ jobs: # Run Linter against code base # ################################ - name: Super-Linter - uses: github/super-linter@v4.8.1 + uses: github/super-linter@v4.9.5 env: VALIDATE_ALL_CODEBASE: false diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 8f97781f..836da94a 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -1,18 +1,73 @@ +--- name: Unit Tests and Coverage on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: unit-tests: - runs-on: ubuntu-latest - # If we are going to use a prebuilt image like this we need a webwork repository on docker hub - container: drgrice1/webwork3 + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - name: Checkout webwork3 source code + uses: actions/checkout@v3 + + # Disabling these things speeds up the setup considerably, and they are not needed for the throw away machine. + - name: Disable man-db and initramfs updates + run: | + sudo sed -i 's/yes/no/g' /etc/initramfs-tools/update-initramfs.conf + sudo rm -f /var/lib/man-db/auto-update + + - name: Install dependencies + env: + DEBIAN_FRONTEND: noninteractive + DEBCONF_NONINTERACTIVE_SEEN: true + DEBCONF_NOWARNINGS: yes + run: | + sudo apt-get update + sudo apt-get install -qy --no-install-recommends --no-install-suggests \ + cpanminus \ + libarray-utils-perl \ + libcanary-stability-perl \ + libcapture-tiny-perl \ + libclass-accessor-lite-perl \ + libclone-perl \ + libcpanel-json-xs-perl \ + libcrypt-ssleay-perl \ + libdata-dump-perl \ + libdatetime-format-strptime-perl \ + libdbd-sqlite3-perl \ + libdbix-class-inflatecolumn-serializer-perl \ + libdbix-class-perl \ + libdbix-dbschema-perl \ + libdevel-cover-perl \ + libexception-class-perl \ + libextutils-config-perl \ + libextutils-helpers-perl \ + libextutils-installpaths-perl \ + libfurl-perl \ + libhttp-parser-xs-perl \ + libimporter-perl \ + libio-socket-ssl-perl \ + liblist-moreutils-perl \ + libmodule-build-tiny-perl \ + libmojolicious-perl \ + libmojolicious-plugin-authentication-perl \ + libnet-ssleay-perl \ + libsql-translator-perl \ + libtest-exception-perl \ + libtest-harness-perl \ + libtext-csv-perl \ + libtry-tiny-perl \ + libyaml-libyaml-perl + cpanm --sudo --notest \ + DBIx::Class::DynamicSubclass \ + Mojolicious::Plugin::DBIC \ + Mojolicious::Plugin::NotYAMLConfig \ + Devel::Cover::Report::Codecov + - name: Run perl unit tests env: HARNESS_PERL_SWITCHES: -MDevel::Cover @@ -20,24 +75,20 @@ jobs: perl t/db/build_db.pl prove -r t - # we probably don'te need to upload the codecov data - # - uses: actions/upload-artifact@v2 - # with: - # name: coverage-report - # path: cover_db/ - - name: Push coverage analysis if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: cover -report codecov - # Install node (for npm) and use it to install eslint and stylelint dependencies. - - name: Use Node.js - uses: actions/setup-node@v2 + # Install node (for npm). + - name: Set up node + uses: actions/setup-node@v3 with: node-version: '16' + - name: Install Dependencies run: npm ci + - name: Run typescript (client-side) tests run: npm run test diff --git a/docker/webwork3.dockerfile b/docker/webwork3.dockerfile index d5ec402f..2216c9a8 100644 --- a/docker/webwork3.dockerfile +++ b/docker/webwork3.dockerfile @@ -1,46 +1,61 @@ -FROM ubuntu:20.04 +FROM ubuntu:22.04 -# the following are needed to make sure the timezone packages don't ask for your timezone. ENV DEBIAN_FRONTEND noninteractive ENV DEBCONF_NONINTERACTIVE_SEEN true +ENV DEBCONF_NOWARNINGS yes ENV HARNESS_PERL_SWITCHES -MDevel::Cover RUN apt-get update && \ apt-get install -qy --no-install-recommends --no-install-suggests \ - build-essential=12.8ubuntu1 \ - ca-certificates=20210119~20.04.1 \ - cpanminus=1.7044-1 \ - git=1:2.25.1-1ubuntu3.1 \ - libarray-utils-perl=0.5-1 \ - libclone-perl=0.43-2 \ - libcrypt-ssleay-perl=0.73.06-1build3 \ - libdata-dump-perl=1.23-1 \ - libdatetime-format-strptime-perl=1.7600-1 \ - libdbd-mysql-perl=4.050-3 \ - libdbd-sqlite3-perl=1.64-1build1 \ - libdbix-class-perl=0.082841-1 \ + ca-certificates=20211016 \ + cpanminus=1.7045-1 \ + git=1:2.34.1-1ubuntu1.4 \ + libarray-utils-perl=0.5-2 \ + libcanary-stability-perl=2006-2 \ + libcapture-tiny-perl=0.48-1 \ + libclass-accessor-lite-perl=0.08-1.1 \ + libclone-perl=0.45-1build3 \ + libcommon-sense-perl=3.75-2build1 \ + libcpanel-json-xs-perl=4.27-1build1 \ + libcrypt-ssleay-perl=0.73.06-1build6 \ + libdata-dump-perl=1.25-1 \ + libdatetime-format-strptime-perl=1.7900-1 \ + #libdbd-mysql-perl=4.050-5 \ # if desired to use a full database + libdbd-sqlite3-perl=1.70-3build1 \ libdbix-class-inflatecolumn-serializer-perl=0.09-1 \ + libdbix-class-perl=0.082842-3 \ libdbix-dbschema-perl=0.45-1 \ - libdevel-cover-perl=1.33-1build1 \ - libexception-class-perl=1.44-1 \ - libjson-perl=4.02000-2 \ - libnet-ssleay-perl=1.88-2ubuntu1 \ - libsql-translator-perl=1.60-1 \ - libssl-dev=1.1.1f-1ubuntu2.5 \ + libdevel-cover-perl=1.36-2build2 \ + libc6-dev=2.35-0ubuntu3.1 \ + libexception-class-perl=1.45-1 \ + libextutils-config-perl=0.008-2 \ + libextutils-helpers-perl=0.026-1 \ + libextutils-installpaths-perl=0.012-1.1 \ + libfurl-perl=3.14-2 \ + libhttp-parser-xs-perl=0.17-2build1 \ + libimporter-perl=0.026-1 \ + libio-socket-ssl-perl=2.074-2 \ + libjson-perl=4.04000-1 \ + libjson-xs-perl=4.030-1build3 \ + libmodule-build-tiny-perl=0.039-1.1 \ + libmojolicious-perl=9.22+dfsg-1 \ + libmojolicious-plugin-authentication-perl=1.37-1 \ + libnet-ssleay-perl=1.92-1build2 \ + libsql-translator-perl=1.62-1 \ + libssl-dev=3.0.2-0ubuntu1.6 \ libtest-exception-perl=0.43-1 \ - libtest-harness-perl=3.42-2 \ - libtext-csv-perl=2.00-1 \ - libtry-tiny-perl=0.30-1 \ - libyaml-libyaml-perl=0.81+repack-1 \ - # mariadb-server=1:10.3.31-0ubuntu0.20.04.1 \ # if desired to use a full database - openssl=1.1.1f-1ubuntu2.5 && \ + libtext-csv-perl=2.01-1 \ + libtry-tiny-perl=0.31-1 \ + libtypes-serialiser-perl=1.01-1 \ + libyaml-libyaml-perl=0.83+ds-1build1 \ + make=4.3-4.1build1 \ + #mariadb-server=1:10.6.7-2ubuntu1.1 \ # if desired to use a full database + openssl=3.0.2-0ubuntu1.6 && \ rm -rf /var/lib/apt/lists/* && \ cpanm --notest \ DBIx::Class::DynamicSubclass \ - Mojolicious \ - Mojolicious::Plugin::NotYAMLConfig \ Mojolicious::Plugin::DBIC \ - Mojolicious::Plugin::Authentication \ + Mojolicious::Plugin::NotYAMLConfig \ Devel::Cover::Report::Codecov ENTRYPOINT ["/bin/bash"] diff --git a/lib/DB/Schema/Result/Attempt.pm b/lib/DB/Schema/Result/Attempt.pm index ff879e1f..0b6ada18 100644 --- a/lib/DB/Schema/Result/Attempt.pm +++ b/lib/DB/Schema/Result/Attempt.pm @@ -59,7 +59,7 @@ Note: a problem should have only one of a library_id, problem_path or problem_po __PACKAGE__->table('attempt'); -__PACKAGE__->load_components('InflateColumn::Serializer', 'Core'); +__PACKAGE__->load_components(qw/InflateColumn::Serializer Core/); __PACKAGE__->add_columns( attempt_id => { diff --git a/lib/DB/Schema/Result/CourseSettings.pm b/lib/DB/Schema/Result/CourseSettings.pm index eacdb07e..fbcbc0a7 100644 --- a/lib/DB/Schema/Result/CourseSettings.pm +++ b/lib/DB/Schema/Result/CourseSettings.pm @@ -49,7 +49,7 @@ C: a JSON object that stores email settings __PACKAGE__->table('course_settings'); -__PACKAGE__->load_components('InflateColumn::Serializer', 'Core'); +__PACKAGE__->load_components(qw/InflateColumn::Serializer Core/); __PACKAGE__->add_columns( course_settings_id => { diff --git a/lib/DB/Schema/Result/CourseUser.pm b/lib/DB/Schema/Result/CourseUser.pm index 123266eb..ceb52c97 100644 --- a/lib/DB/Schema/Result/CourseUser.pm +++ b/lib/DB/Schema/Result/CourseUser.pm @@ -88,7 +88,7 @@ C: whether or not the user shows old answer (boolean) __PACKAGE__->table('course_user'); -__PACKAGE__->load_components('InflateColumn::Serializer', 'Core'); +__PACKAGE__->load_components(qw/InflateColumn::Serializer Core/); __PACKAGE__->add_columns( course_user_id => { diff --git a/lib/DB/Schema/Result/PoolProblem.pm b/lib/DB/Schema/Result/PoolProblem.pm index 13855576..728e4706 100644 --- a/lib/DB/Schema/Result/PoolProblem.pm +++ b/lib/DB/Schema/Result/PoolProblem.pm @@ -48,7 +48,7 @@ Note: the C can only have one of the two fields __PACKAGE__->table('pool_problem'); -__PACKAGE__->load_components('InflateColumn::Serializer', 'Core'); +__PACKAGE__->load_components(qw/InflateColumn::Serializer Core/); __PACKAGE__->add_columns( pool_problem_id => { diff --git a/lib/DB/Schema/Result/ProblemSet.pm b/lib/DB/Schema/Result/ProblemSet.pm index e2f620dc..57664aef 100644 --- a/lib/DB/Schema/Result/ProblemSet.pm +++ b/lib/DB/Schema/Result/ProblemSet.pm @@ -71,11 +71,9 @@ L which gives properties common to re =cut -__PACKAGE__->load_components(qw/DynamicSubclass Core/); - __PACKAGE__->table('problem_set'); -__PACKAGE__->load_components(qw/DynamicSubclass Core/, qw/InflateColumn::Serializer Core/); +__PACKAGE__->load_components(qw/DynamicSubclass Core InflateColumn::Serializer InflateColumn::Boolean Core/); __PACKAGE__->add_columns( set_id => { diff --git a/lib/DB/Schema/Result/SetProblem.pm b/lib/DB/Schema/Result/SetProblem.pm index 00c4855c..8b616373 100644 --- a/lib/DB/Schema/Result/SetProblem.pm +++ b/lib/DB/Schema/Result/SetProblem.pm @@ -65,7 +65,7 @@ Note: a problem should have only one of a library_id, problem_path or problem_po __PACKAGE__->table('set_problem'); -__PACKAGE__->load_components('InflateColumn::Serializer', 'Core'); +__PACKAGE__->load_components(qw/InflateColumn::Serializer Core/); __PACKAGE__->add_columns( set_problem_id => { diff --git a/lib/DB/Schema/Result/UserProblem.pm b/lib/DB/Schema/Result/UserProblem.pm index 5633ee7d..381c3480 100644 --- a/lib/DB/Schema/Result/UserProblem.pm +++ b/lib/DB/Schema/Result/UserProblem.pm @@ -15,7 +15,7 @@ use base qw/DBIx::Class::Core DB::Validation/; __PACKAGE__->table('user_problem'); -__PACKAGE__->load_components('InflateColumn::Serializer', 'Core'); +__PACKAGE__->load_components(qw/InflateColumn::Serializer Core/); __PACKAGE__->add_columns( user_problem_id => { diff --git a/lib/DB/Schema/Result/UserSet.pm b/lib/DB/Schema/Result/UserSet.pm index 2bed2a10..4df5a9a8 100644 --- a/lib/DB/Schema/Result/UserSet.pm +++ b/lib/DB/Schema/Result/UserSet.pm @@ -58,7 +58,7 @@ types have different params fields. __PACKAGE__->table('user_set'); -__PACKAGE__->load_components('InflateColumn::Serializer', 'Core'); +__PACKAGE__->load_components(qw/InflateColumn::Serializer InflateColumn::Boolean Core/); __PACKAGE__->add_columns( user_set_id => { diff --git a/t/db/001_courses.t b/t/db/001_courses.t index 0b2b72d8..65f1b7bb 100644 --- a/t/db/001_courses.t +++ b/t/db/001_courses.t @@ -18,6 +18,7 @@ use Test::More; use Test::Exception; use YAML::XS qw/LoadFile/; use DateTime::Format::Strptime; +use Mojo::JSON qw/true false/; use DB::Schema; @@ -90,7 +91,7 @@ throws_ok { # Add a course my $new_course_params = { course_name => 'Geometry', - visible => 1, + visible => true, course_dates => {} }; diff --git a/t/db/004_course_users.t b/t/db/004_course_users.t index b79c4aa9..0aca1109 100644 --- a/t/db/004_course_users.t +++ b/t/db/004_course_users.t @@ -42,7 +42,7 @@ my $user_rs = $schema->resultset('User'); # Get a list of users from the CSV file my @students = loadCSV("$main::ww3_dir/t/db/sample_data/students.csv"); for my $student (@students) { - $student->{is_admin} = 0; + $student->{is_admin} = false; $student->{course_user_params} = $student->{params}; delete $student->{params}; } @@ -124,7 +124,7 @@ my $user_params = { last_name => 'Quimby', email => 'mayor_joe@springfield.gov', student_id => '12345', - is_admin => 0 + is_admin => false }; my $course_user_params = { diff --git a/t/db/006_quizzes.t b/t/db/006_quizzes.t index 7ca3abc8..0a89267f 100644 --- a/t/db/006_quizzes.t +++ b/t/db/006_quizzes.t @@ -45,7 +45,13 @@ my $user_rs = $schema->resultset('User'); my @all_problem_sets; -my @quizzes = loadCSV("$main::ww3_dir/t/db/sample_data/quizzes.csv"); +my @quizzes = loadCSV( + "$main::ww3_dir/t/db/sample_data/quizzes.csv", + { + boolean_fields => ['set_visible'], + param_boolean_fields => ['timed'] + } +); for my $quiz (@quizzes) { $quiz->{set_type} = 'QUIZ'; } @@ -248,7 +254,7 @@ my $updated_quiz = $problem_set_rs->updateProblemSet( params => $updated_params ); -$new_quiz->{set_visible} = 0; +$new_quiz->{set_visible} = false; $new_quiz->{set_params} = {}; removeIDs($updated_quiz); is_deeply($new_quiz, $updated_quiz, 'updateQuiz: successfully update the quiz'); diff --git a/t/mojolicious/001_login.t b/t/mojolicious/001_login.t index 6efa4da1..8b1c4507 100644 --- a/t/mojolicious/001_login.t +++ b/t/mojolicious/001_login.t @@ -5,8 +5,7 @@ use Mojo::Base -strict; use Test::More; use Test::Mojo; use YAML::XS qw/LoadFile/; - -use YAML::XS qw/LoadFile/; +use Mojo::JSON qw/true false/; BEGIN { use File::Basename qw/dirname/; @@ -36,11 +35,11 @@ $t->post_ok('/webwork3/api/login')->status_is(500, 'error status')->content_type $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) ->content_type_is('application/json;charset=UTF-8')->json_is( '' => { - logged_in => 1, + logged_in => true, user => { email => 'lisa@google.com', first_name => 'Lisa', - is_admin => 0, + is_admin => false, last_name => 'Simpson', student_id => '23', user_id => 3, @@ -53,7 +52,7 @@ $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => ' # Test logout $t->post_ok('/webwork3/api/logout')->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is( '' => { - logged_in => 0, + logged_in => false, message => 'Successfully logged out.' }, 'logout' @@ -63,7 +62,7 @@ $t->post_ok('/webwork3/api/logout')->status_is(200)->content_type_is('applicatio $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'wrong_password' })->status_is(200) ->content_type_is('application/json;charset=UTF-8')->json_is( '' => { - logged_in => 0, + logged_in => false, message => 'Incorrect username or password.' }, 'invalid credentials' diff --git a/t/mojolicious/002_courses.t b/t/mojolicious/002_courses.t index bf84f35a..0626865a 100644 --- a/t/mojolicious/002_courses.t +++ b/t/mojolicious/002_courses.t @@ -28,14 +28,14 @@ my $t = Test::Mojo->new(WeBWorK3 => $config); # Authenticate with the admin user. $t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => 1)->json_is('/user/user_id' => 1) - ->json_is('/user/is_admin' => 1); + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true)->json_is('/user/user_id' => 1) + ->json_is('/user/is_admin' => true); $t->get_ok('/webwork3/api/courses')->content_type_is('application/json;charset=UTF-8') - ->json_is('/0/course_name' => 'Precalculus')->json_is('/0/visible' => 1); + ->json_is('/0/course_name' => 'Precalculus')->json_is('/0/visible' => true); $t->get_ok('/webwork3/api/courses/1')->content_type_is('application/json;charset=UTF-8') - ->json_is('/course_name' => 'Precalculus')->json_is('/visible' => 1); + ->json_is('/course_name' => 'Precalculus')->json_is('/visible' => true); # Add a new course my $new_course = { @@ -124,8 +124,8 @@ $t->get_ok('/webwork3/api/courses/4/settings')->status_is(200)->content_type_is( $t->post_ok('/webwork3/api/courses' => json => $new_course)->status_is(403)->json_is('/has_permission' => 0); $t->put_ok('/webwork3/api/courses/4' => json => { course_name => 'XXX' })->status_is(403) - ->json_is('/has_permission' => false); + ->json_is('/has_permission' => 0); -$t->delete_ok('/webwork3/api/courses/4')->status_is(403)->json_is('/has_permission' => false); +$t->delete_ok('/webwork3/api/courses/4')->status_is(403)->json_is('/has_permission' => 0); done_testing; diff --git a/t/mojolicious/003_users.t b/t/mojolicious/003_users.t index 1e2ded44..b35681ec 100644 --- a/t/mojolicious/003_users.t +++ b/t/mojolicious/003_users.t @@ -38,8 +38,8 @@ my $t = Test::Mojo->new(WeBWorK3 => $config); # Test all of the user routes with an admin user. $t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => 1)->json_is('/user/user_id' => 1) - ->json_is('/user/is_admin' => 1); + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true)->json_is('/user/user_id' => 1) + ->json_is('/user/is_admin' => true); my @all_users = $schema->resultset('User')->getAllGlobalUsers(); @@ -56,7 +56,7 @@ my $new_user = { last_name => 'Simpson', username => 'maggie', student_id => '1234123423', - is_admin => 0 + is_admin => false }; $t->post_ok('/webwork3/api/users' => json => $new_user)->status_is(200) @@ -158,10 +158,10 @@ $t->delete_ok("/webwork3/api/users/$another_new_user_id")->status_is(200) # Test that a non-admin user cannot access all of the routes # Logout the admin user and relogin as a non-admin. -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => 0); +$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => 1) - ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => 0); + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); $t->get_ok('/webwork3/api/users')->content_type_is('application/json;charset=UTF-8')->status_is(403) ->json_is('/has_permission' => 0); @@ -182,7 +182,7 @@ $t->delete_ok('/webwork3/api/users/1')->content_type_is('application/json;charse $t->get_ok('/webwork3/api/users/3/courses')->status_is(200)->content_type_is('application/json;charset=UTF-8'); # Relogin as the admin and delete the added users -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => 0); +$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); $t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200); # The following routes test that global users can be handled by an instructor in the course diff --git a/t/mojolicious/008_problems.t b/t/mojolicious/008_problems.t index 72fbb83c..0e130aa5 100644 --- a/t/mojolicious/008_problems.t +++ b/t/mojolicious/008_problems.t @@ -41,8 +41,8 @@ my $t = Test::Mojo->new(WeBWorK3 => $config); # First run tests as logged in as an instructor $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => 1) - ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => 0); + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); # Load all problems from the CVS files. my @problems_from_csv = loadCSV( @@ -111,10 +111,10 @@ $t->put_ok( ->json_is('/problem_number' => $new_problem->{problem_number})->json_is('/problem_params/weight' => 3); # Make sure that a student cannot access the global problem routes -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => 0); +$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); $t->post_ok('/webwork3/api/login' => json => { username => 'ralph', password => 'ralph' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => 1) - ->json_is('/user/username' => 'ralph')->json_is('/user/is_admin' => 0); + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'ralph')->json_is('/user/is_admin' => false); my $logged_in_user = $t->tx->res->json('/user'); @@ -157,10 +157,10 @@ $t->delete_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem # Make sure that a student not enrolled in the course has access to getting global problems # for that course. -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => 0); +$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); $t->post_ok('/webwork3/api/login' => json => { username => 'ned', password => 'ned' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => 1)->json_is('/user/username' => 'ned') - ->json_is('/user/is_admin' => 0); + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'ned')->json_is('/user/is_admin' => false); $logged_in_user = $t->tx->res->json('/user'); @@ -174,7 +174,7 @@ is(scalar(grep { $_->{course_name} eq 'Arithmetic' } @$user_courses), 0, 'The us $t->get_ok('/webwork3/api/courses/4/problems')->status_is(403)->content_type_is('application/json;charset=UTF-8'); # Finally, delete the new problem to restore the db to it's pretest state. -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => 0); +$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); $t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200); $t->delete_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}")->status_is(200) diff --git a/t/mojolicious/009_user_problems.t b/t/mojolicious/009_user_problems.t index 7066715a..1210baab 100644 --- a/t/mojolicious/009_user_problems.t +++ b/t/mojolicious/009_user_problems.t @@ -41,8 +41,8 @@ my $t = Test::Mojo->new(WeBWorK3 => $config); # First run tests as logged in as an instructor $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => 1) - ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => 0); + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); # Load all problems from the CVS files. my @problems_from_csv = loadCSV( @@ -174,7 +174,7 @@ $t->put_ok( ->json_is('/seed' => 789); # Check that a student has the correct access -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => 0); +$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); $t->post_ok('/webwork3/api/login' => json => { username => 'ralph', password => 'ralph' })->status_is(200); # get all of ralph's problems @@ -208,7 +208,7 @@ $t->delete_ok( ->status_is(403); # Make sure that a user that is in the course, cannot get or update a user problem that is not one's own. -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => 0); +$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); $t->post_ok('/webwork3/api/login' => json => { username => 'moe', password => 'moe' })->status_is(200); # Check that moe is in the course @@ -233,7 +233,7 @@ $t->put_ok( => json => { status => 0.5 })->status_is(403); # Switch back to the instructor and delete the user problem -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => 0); +$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200); $t->delete_ok( diff --git a/t/mojolicious/010_problem_pools.t b/t/mojolicious/010_problem_pools.t index 83e3e992..917780b0 100644 --- a/t/mojolicious/010_problem_pools.t +++ b/t/mojolicious/010_problem_pools.t @@ -150,7 +150,7 @@ $t->put_ok( )->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/params/library_id' => 8932); # Make sure that students don't have access to Problem Pools -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => 0); +$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); $t->post_ok('/webwork3/api/login' => json => { username => 'ralph', password => 'ralph' })->status_is(200); $t->get_ok('/webwork3/api/courses/4/pools')->status_is(403); @@ -189,7 +189,7 @@ $t->delete_ok( )->status_is(403); # Cleanup. Log back in as the instructor and delete added pool and problem -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => 0); +$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200); # Delete the pool problem. From eec4b1965c0126c2b7d185fbab3cc2852074c5b0 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sat, 27 Aug 2022 19:28:28 -0500 Subject: [PATCH 2/3] Switch to using the Test2::Suite and Test2::MojoX modules for unit tests. Note that is important for the unit tests to have the `got` argument first and the `expected` argument second in an `is` test. Test2::Tools::Compare converts the second `expected` argument into a Test2::Tools object which is important for strict checks. Also remove the incomplete t/db/test_course_user.pl file. --- .github/workflows/unit-tests.yml | 5 +- .perlcriticrc | 7 + docker/webwork3.dockerfile | 7 +- t/db/001_courses.t | 104 ++-- t/db/002_course_settings.t | 220 ++++---- t/db/003_users.t | 167 +++--- t/db/004_course_users.t | 336 ++++++------ t/db/005_hwsets.t | 295 +++++----- t/db/006_quizzes.t | 437 +++++++-------- t/db/007_user_set.t | 445 +++++++-------- t/db/008_problem_pools.t | 230 ++++---- t/db/009_problems.t | 251 ++++----- t/db/010_user_problems.t | 875 ++++++++++++++++-------------- t/db/011_attempts.t | 24 +- t/db/012_set_versions.t | 17 +- t/db/013_problem_versions.t | 17 +- t/db/README.md | 2 +- t/db/test_course_user.pl | 35 -- t/mojolicious/001_login.t | 6 +- t/mojolicious/002_courses.t | 10 +- t/mojolicious/003_users.t | 12 +- t/mojolicious/004_course_users.t | 6 +- t/mojolicious/005_problem_sets.t | 10 +- t/mojolicious/006_quizzes.t | 10 +- t/mojolicious/007_review_sets.t | 8 +- t/mojolicious/008_problems.t | 10 +- t/mojolicious/009_user_problems.t | 15 +- t/mojolicious/010_problem_pools.t | 10 +- 28 files changed, 1729 insertions(+), 1842 deletions(-) delete mode 100755 t/db/test_course_user.pl diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 836da94a..42ca9be3 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -57,8 +57,10 @@ jobs: libmojolicious-plugin-authentication-perl \ libnet-ssleay-perl \ libsql-translator-perl \ - libtest-exception-perl \ + libsub-info-perl \ + libterm-table-perl \ libtest-harness-perl \ + libtest2-suite-perl \ libtext-csv-perl \ libtry-tiny-perl \ libyaml-libyaml-perl @@ -66,6 +68,7 @@ jobs: DBIx::Class::DynamicSubclass \ Mojolicious::Plugin::DBIC \ Mojolicious::Plugin::NotYAMLConfig \ + Test2::MojoX \ Devel::Cover::Report::Codecov - name: Run perl unit tests diff --git a/.perlcriticrc b/.perlcriticrc index 5a28ef7f..b3d5fb42 100644 --- a/.perlcriticrc +++ b/.perlcriticrc @@ -5,3 +5,10 @@ severity = 4 # Allow no warnings usage with a category restriction (for signatures) [TestingAndDebugging::ProhibitNoWarnings] allow_with_category_restriction = 1 + +# Allow $a and $b in sort functions. If sort functions are added to the code not in a "sort" call, they must be added +# to this list. Annoyingly both of these policies that do the same thing have to each get the list. +[Community::DollarAB] +extra_pair_functions = user_prob_sort_fxn +[Freenode::DollarAB] +extra_pair_functions = user_prob_sort_fxn diff --git a/docker/webwork3.dockerfile b/docker/webwork3.dockerfile index 2216c9a8..334ad370 100644 --- a/docker/webwork3.dockerfile +++ b/docker/webwork3.dockerfile @@ -11,6 +11,7 @@ RUN apt-get update && \ cpanminus=1.7045-1 \ git=1:2.34.1-1ubuntu1.4 \ libarray-utils-perl=0.5-2 \ + libc6-dev=2.35-0ubuntu3.1 \ libcanary-stability-perl=2006-2 \ libcapture-tiny-perl=0.48-1 \ libclass-accessor-lite-perl=0.08-1.1 \ @@ -26,7 +27,6 @@ RUN apt-get update && \ libdbix-class-perl=0.082842-3 \ libdbix-dbschema-perl=0.45-1 \ libdevel-cover-perl=1.36-2build2 \ - libc6-dev=2.35-0ubuntu3.1 \ libexception-class-perl=1.45-1 \ libextutils-config-perl=0.008-2 \ libextutils-helpers-perl=0.026-1 \ @@ -43,7 +43,11 @@ RUN apt-get update && \ libnet-ssleay-perl=1.92-1build2 \ libsql-translator-perl=1.62-1 \ libssl-dev=3.0.2-0ubuntu1.6 \ + libsub-info-perl=0.015-2 \ + libterm-table-perl=0.015-2 \ libtest-exception-perl=0.43-1 \ + libtest-harness-perl=3.42-2 \ + libtest2-suite-perl=0.000144-1 \ libtext-csv-perl=2.01-1 \ libtry-tiny-perl=0.31-1 \ libtypes-serialiser-perl=1.01-1 \ @@ -56,6 +60,7 @@ RUN apt-get update && \ DBIx::Class::DynamicSubclass \ Mojolicious::Plugin::DBIC \ Mojolicious::Plugin::NotYAMLConfig \ + Test2::MojoX \ Devel::Cover::Report::Codecov ENTRYPOINT ["/bin/bash"] diff --git a/t/db/001_courses.t b/t/db/001_courses.t index 65f1b7bb..c5a5ffdf 100644 --- a/t/db/001_courses.t +++ b/t/db/001_courses.t @@ -14,15 +14,13 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; +use Test2::V0; use YAML::XS qw/LoadFile/; -use DateTime::Format::Strptime; -use Mojo::JSON qw/true false/; +use Mojo::JSON qw/true/; use DB::Schema; -use TestUtils qw/loadCSV removeIDs loadSchema/; +use TestUtils qw/loadCSV removeIDs/; # Load the database my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; @@ -34,7 +32,6 @@ my $schema = DB::Schema->connect( $config->{database_password}, { quote_names => 1 } ); -my $strp = DateTime::Format::Strptime->new(pattern => '%F', on_error => 'croak'); my $course_rs = $schema->resultset('Course'); @@ -56,56 +53,56 @@ my @courses_from_db = $course_rs->getCourses; for my $course (@courses_from_db) { removeIDs($course); } @courses_from_db = sortByCourseName(\@courses_from_db); -is_deeply(\@courses_from_db, \@courses, 'getCourses: get all courses'); +is(\@courses_from_db, \@courses, 'getCourses: get all courses'); -## Get a single course by name +# Get a single course by name my $course = $course_rs->getCourse(info => { course_name => 'Calculus' }); my $calc_id = $course->{course_id}; delete $course->{course_id}; my @calc_courses = grep { $_->{course_name} eq 'Calculus' } @courses; -is_deeply($course, $calc_courses[0], 'getCourse: get a single course by name'); +is($course, $calc_courses[0], 'getCourse: get a single course by name'); # Get a single course by course_id $course = $course_rs->getCourse(info => { course_id => $calc_id }); delete $course->{course_id}; -is_deeply($course, $calc_courses[0], 'getCourse: get a single course by id'); +is($course, $calc_courses[0], 'getCourse: get a single course by id'); # Try to get a single course by sending proper info: -throws_ok { - $course_rs->getCourse(info => { course_id => $calc_id, course_name => 'Calculus' }); -} -'DB::Exception::ParametersNeeded', 'getCourse: sends too much info'; +is( + dies { $course_rs->getCourse(info => { course_id => $calc_id, course_name => 'Calculus' }); }, + check_isa('DB::Exception::ParametersNeeded'), + 'getCourse: sends too much info' +); -throws_ok { - $course_rs->getCourse(info => { name => 'Calculus' }); -} -'DB::Exception::ParametersNeeded', 'getCourse: sends wrong info'; +is( + dies { $course_rs->getCourse(info => { name => 'Calculus' }); }, + check_isa('DB::Exception::ParametersNeeded'), + 'getCourse: sends wrong info' +); # Try to get a single course that doesn't exist -throws_ok { - $course_rs->getCourse(info => { course_name => 'non_existent_course' }); -} -'DB::Exception::CourseNotFound', 'getCourse: get a non-existent course'; +is( + dies { $course_rs->getCourse(info => { course_name => 'non_existent_course' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getCourse: get a non-existent course' +); # Add a course -my $new_course_params = { - course_name => 'Geometry', - visible => true, - course_dates => {} -}; +my $new_course_params = { course_name => 'Geometry', visible => true, course_dates => {} }; my $new_course = $course_rs->addCourse(params => $new_course_params); my $added_course_id = $new_course->{course_id}; removeIDs($new_course); -is_deeply($new_course_params, $new_course, 'addCourse: add a new course'); +is($new_course, $new_course_params, 'addCourse: add a new course'); # Add a course that already exists -throws_ok { - $course_rs->addCourse(params => { course_name => 'Geometry', visible => 1 }); -} -'DB::Exception::CourseAlreadyExists', 'addCourse: course already exists'; +is( + dies { $course_rs->addCourse(params => { course_name => 'Geometry', visible => true }); }, + check_isa('DB::Exception::CourseAlreadyExists'), + 'addCourse: course already exists' +); # Update the course name my $updated_course = $course_rs->updateCourse( @@ -116,36 +113,40 @@ my $updated_course = $course_rs->updateCourse( $new_course_params->{course_name} = 'Geometry II'; delete $updated_course->{course_id}; -is_deeply($new_course_params, $updated_course, 'updateCourse: update a course by name'); +is($updated_course, $new_course_params, 'updateCourse: update a course by name'); # Try to update an non-existent course -throws_ok { - $course_rs->updateCourse(info => { course_name => 'non_existent_course' }); -} -'DB::Exception::CourseNotFound', 'updateCourse: update a non-existent course_name'; +is( + dies { $course_rs->updateCourse(info => { course_name => 'non_existent_course' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'updateCourse: update a non-existent course_name' +); -throws_ok { - $course_rs->updateCourse(info => { course_id => -9 }, params => $new_course_params); -} -'DB::Exception::CourseNotFound', 'updateCourse: update a non-existent course_id'; +is( + dies { $course_rs->updateCourse(info => { course_id => -9 }, params => $new_course_params); }, + check_isa('DB::Exception::CourseNotFound'), + 'updateCourse: update a non-existent course_id' +); # Delete a course my $deleted_course = $course_rs->deleteCourse(info => { course_name => 'Geometry II' }); removeIDs($deleted_course); -is_deeply($new_course_params, $deleted_course, 'deleteCourse: delete a course'); +is($deleted_course, $new_course_params, 'deleteCourse: delete a course'); # Try to delete a non-existent course by name -throws_ok { - $course_rs->deleteCourse(info => { course_name => 'undefined_name' }) -} -'DB::Exception::CourseNotFound', 'deleteCourse: delete a non-existent course_name'; +is( + dies { $course_rs->deleteCourse(info => { course_name => 'undefined_name' }) }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteCourse: delete a non-existent course_name' +); # Try to delete a non-existent course by id -throws_ok { - $course_rs->deleteCourse(info => { course_id => -9 }) -} -'DB::Exception::CourseNotFound', 'deleteCourse: delete a non-existent course_id'; +is( + dies { $course_rs->deleteCourse(info => { course_id => -9 }) }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteCourse: delete a non-existent course_id' +); sub sortByCourseName { my $course_rs = shift; @@ -158,7 +159,6 @@ sub sortByCourseName { for my $course (@courses_from_db) { removeIDs($course); } @courses_from_db = sortByCourseName(\@courses_from_db); -is_deeply(\@courses_from_db, \@courses, 'check: courses db table is returned to its original state.'); +is(\@courses_from_db, \@courses, 'check: courses db table is returned to its original state.'); done_testing(); - diff --git a/t/db/002_course_settings.t b/t/db/002_course_settings.t index 72334a12..1f5a299e 100644 --- a/t/db/002_course_settings.t +++ b/t/db/002_course_settings.t @@ -14,14 +14,13 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; +use Test2::V0; use YAML::XS qw/LoadFile/; use DB::Schema; use WeBWorK3::Utils::Settings qw/getDefaultCourseSettings getDefaultCourseValues - validateSettingsConfFile validateSingleCourseSetting validateSettingConfig + validateSettingsConfFile validateSettingConfig isInteger isTimeString isTimeDuration isDecimal mergeCourseSettings/; use TestUtils qw/removeIDs loadSchema/; @@ -96,94 +95,118 @@ is(validateSettingConfig($valid_setting), 1, 'course setting: valid setting'); # Check various parts of the setting. -throws_ok { - validateSettingConfig({ - var => 'mySetting', - doc => 'this is a setting', - type => 'integer', - category => 'general', - default => 0 - }) -} -'DB::Exception::InvalidCourseField', 'course setting: variable not in kebob case'; - -throws_ok { - validateSettingConfig({ - var => 'my_setting', - doc3 => 'this is a setting', - type => 'integer', - category => 'general', - default => 0 - }) -} -'DB::Exception::InvalidCourseField', 'course setting: course setting with illegal field'; - -throws_ok { - validateSettingConfig({ - var => 'my_setting', - type => 'integer', - category => 'general', - default => 0 - }) -} -'DB::Exception::InvalidCourseField', 'course setting: missing required field'; - -throws_ok { - validateSettingConfig({ - var => 'my_setting', - doc => 'this is a setting', - type => 'nonnegint', - category => 'general', - default => 0 - }) -} -'DB::Exception::InvalidCourseFieldType', 'course setting: non valid course parameter type'; +is( + dies { + validateSettingConfig({ + var => 'mySetting', + doc => 'this is a setting', + type => 'integer', + category => 'general', + default => 0 + }) + }, + check_isa('DB::Exception::InvalidCourseField'), + 'course setting: variable not in kebob case' +); + +is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc3 => 'this is a setting', + type => 'integer', + category => 'general', + default => 0 + }) + }, + check_isa('DB::Exception::InvalidCourseField'), + 'course setting: course setting with illegal field' +); + +is( + dies { + validateSettingConfig({ + var => 'my_setting', + type => 'integer', + category => 'general', + default => 0 + }) + }, + check_isa('DB::Exception::InvalidCourseField'), + 'course setting: missing required field' +); + +is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc => 'this is a setting', + type => 'nonnegint', + category => 'general', + default => 0 + }) + }, + check_isa('DB::Exception::InvalidCourseFieldType'), + 'course setting: non valid course parameter type' +); # Validate settings -throws_ok { - validateSettingConfig({ - var => 'my_setting', - doc => 'this is a setting', - type => 'time', - category => 'general', - default => '12:343' - }) -} -'DB::Exception::InvalidCourseFieldType', 'course setting: bad time string'; - -throws_ok { - validateSettingConfig({ - var => 'my_setting', - doc => 'this is a setting', - type => 'integer', - category => 'general', - default => '12.343' - }) -} -'DB::Exception::InvalidCourseFieldType', 'course setting: bad integer format'; - -throws_ok { - validateSettingConfig({ - var => 'my_setting', - doc => 'this is a setting', - type => 'time_duration', - category => 'general', - default => '-2 days' - }) -} -'DB::Exception::InvalidCourseFieldType', 'course setting: bad time duration format'; - -throws_ok { - validateSettingConfig({ - var => 'my_setting', - doc => 'this is a setting', - type => 'decimal', - category => 'general', - default => '12:343' - }) -} -'DB::Exception::InvalidCourseFieldType', 'course setting: bad decimal format'; +is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc => 'this is a setting', + type => 'time', + category => 'general', + default => '12:343' + }) + }, + check_isa('DB::Exception::InvalidCourseFieldType'), + 'course setting: bad time string' +); + +is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc => 'this is a setting', + type => 'integer', + category => 'general', + default => '12.343' + }) + }, + check_isa('DB::Exception::InvalidCourseFieldType'), + 'course setting: bad integer format' +); + +is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc => 'this is a setting', + type => 'time_duration', + category => 'general', + default => '-2 days' + }) + }, + check_isa('DB::Exception::InvalidCourseFieldType'), + 'course setting: bad time duration format' +); + +is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc => 'this is a setting', + type => 'decimal', + category => 'general', + default => '12:343' + }) + }, + check_isa('DB::Exception::InvalidCourseFieldType'), + 'course setting: bad decimal format' +); my $course_rs = $schema->resultset('Course'); @@ -196,7 +219,7 @@ my $default_course_values = getDefaultCourseValues(); my $new_course_info = { course_id => $new_course->{course_id} }; my $course_settings = $course_rs->getCourseSettings(info => $new_course_info); -is_deeply($course_settings, $default_course_values, 'course settings: default course_settings'); +is($course_settings, $default_course_values, 'course settings: default course_settings'); # Set a single course setting in General my $updated_general_setting = { general => { course_description => 'This is my new course description' } }; @@ -206,7 +229,7 @@ my $updated_course_settings = $course_rs->updateCourseSettings( ); my $current_course_values = mergeCourseSettings($default_course_values, $updated_general_setting); -is_deeply($current_course_values, $updated_course_settings, 'course_settings: updated general setting'); +is($current_course_values, $updated_course_settings, 'course_settings: updated general setting'); # Update another general setting $updated_general_setting = { general => { hardcopy_theme => 'One Column' } }; @@ -218,35 +241,36 @@ $updated_course_settings = $course_rs->updateCourseSettings( $current_course_values = mergeCourseSettings($current_course_values, $updated_general_setting); -is_deeply($current_course_values, $updated_course_settings, 'course_settings: updated another general setting'); +is($current_course_values, $updated_course_settings, 'course_settings: updated another general setting'); # Set a single course setting in Optional Modules. my $updated_optional_setting = { optional => { enable_show_me_another => 1 } }; $updated_course_settings = $course_rs->updateCourseSettings(info => $new_course_info, settings => $updated_optional_setting); $current_course_values = mergeCourseSettings($current_course_values, $updated_optional_setting); -is_deeply($current_course_values, $updated_course_settings, 'course_settings: updated optional setting'); +is($current_course_values, $updated_course_settings, 'course_settings: updated optional setting'); # Set a single course setting in problem_set. my $updated_problem_set_setting = { problem_set => { time_assign_due => '11:52' } }; $updated_course_settings = $course_rs->updateCourseSettings(info => $new_course_info, settings => $updated_problem_set_setting); $current_course_values = mergeCourseSettings($current_course_values, $updated_problem_set_setting); -is_deeply($current_course_values, $updated_course_settings, 'course_settings: updated problem set setting'); +is($current_course_values, $updated_course_settings, 'course_settings: updated problem set setting'); # Set a single course setting in problem. my $updated_problem_setting = { problem => { display_mode => 'images' } }; $updated_course_settings = $course_rs->updateCourseSettings(info => $new_course_info, settings => $updated_problem_setting); $current_course_values = mergeCourseSettings($current_course_values, $updated_problem_setting); -is_deeply($current_course_values, $updated_course_settings, 'course_settings: updated problem setting'); +is($current_course_values, $updated_course_settings, 'course_settings: updated problem setting'); # Make sure that an nonexistant setting throws an exception. my $undefined_problem_setting = { general => { non_existent_setting => 1 } }; -throws_ok { - $course_rs->updateCourseSettings(info => $new_course_info, settings => $undefined_problem_setting); -} -'DB::Exception::UndefinedCourseField', 'course settings: undefined course_setting field'; +is( + dies { $course_rs->updateCourseSettings(info => $new_course_info, settings => $undefined_problem_setting); }, + check_isa('DB::Exception::UndefinedCourseField'), + 'course settings: undefined course_setting field' +); # Make sure that an invalid list option setting throws an exception. my $invalid_list_option = { general => { hardcopy_theme => 'default' } }; diff --git a/t/db/003_users.t b/t/db/003_users.t index ace345db..ce646ab2 100644 --- a/t/db/003_users.t +++ b/t/db/003_users.t @@ -14,12 +14,10 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; +use Test2::V0; use Clone qw/clone/; use YAML qw/LoadFile/; -use DateTime::Format::Strptime; use Mojo::JSON qw/true false/; use DB::Schema; @@ -35,7 +33,6 @@ my $schema = DB::Schema->connect( $config->{database_password}, { quote_names => 1 } ); -my $strp = DateTime::Format::Strptime->new(pattern => '%F', on_error => 'croak'); my $users_rs = $schema->resultset('User'); my $course_rs = $schema->resultset('Course'); @@ -74,36 +71,39 @@ for my $user (@users_from_db) { cleanUndef($user); } @users_from_db = sort { $a->{username} cmp $b->{username} } @users_from_db; -is_deeply(\@all_students, \@users_from_db, 'getUsers: all users'); +is(\@users_from_db, \@all_students, 'getUsers: all users'); # Get a single user by username my $user = $users_rs->getGlobalUser(info => { username => $all_students[0]->{username} }); removeIDs($user); cleanUndef($user); -is_deeply($all_students[0], $user, 'getUser: by username'); +is($user, $all_students[0], 'getUser: by username'); # Get a single user by user_id $user = $users_rs->getGlobalUser(info => { user_id => 2 }); removeIDs($user); my @stud2 = grep { $_->{username} eq $user->{username} } @all_students; -is_deeply($stud2[0], $user, 'getUser: by user_id'); +is($user, $stud2[0], 'getUser: by user_id'); # Get one user that does not exist -throws_ok { - $user = $users_rs->getGlobalUser(info => { user_id => -9 }); -} -'DB::Exception::UserNotFound', 'getUser: undefined user_id'; +is( + dies { $user = $users_rs->getGlobalUser(info => { user_id => -9 }); }, + check_isa('DB::Exception::UserNotFound'), + 'getUser: undefined user_id' +); -throws_ok { - $user = $users_rs->getGlobalUser(info => { username => 'non_existent_user' }); -} -'DB::Exception::UserNotFound', 'getUser: undefined username'; +is( + dies { $user = $users_rs->getGlobalUser(info => { username => 'non_existent_user' }); }, + check_isa('DB::Exception::UserNotFound'), + 'getUser: undefined username' +); # getUsers: Test that not passing either a course_id or course_name results in an error. -throws_ok { - $users_rs->getCourseUsers(info => { my_course => 'Precalculus' }); -} -'DB::Exception::ParametersNeeded', 'getUsers: course_name or course_id not passed in'; +is( + dies { $users_rs->getCourseUsers(info => { my_course => 'Precalculus' }); }, + check_isa('DB::Exception::ParametersNeeded'), + 'getUsers: course_name or course_id not passed in' +); # Add one user $user = { @@ -117,7 +117,7 @@ $user = { my $new_user = $users_rs->addGlobalUser(params => $user); removeIDs($new_user); -is_deeply($user, $new_user, 'addUser: adding a user'); +is($new_user, $user, 'addUser: adding a user'); # Ensure that the default values are set my $patty_params = { username => 'patty' }; @@ -126,18 +126,14 @@ removeIDs($patty); cleanUndef($patty); # the only default for users is { is_admin: false } $patty_params->{is_admin} = false; -is_deeply($patty, $patty_params, 'addUser: check the default values from db.'); +is($patty, $patty_params, 'addUser: check the default values from db.'); # Try to add a user without passing username info -throws_ok { - $users_rs->addGlobalUser( - params => { - username_name => 'selma', - email => 'selma@google.com' - } - ); -} -'DB::Exception::ParametersNeeded', 'addUser: wrong user_info sent'; +is( + dies { $users_rs->addGlobalUser(params => { username_name => 'selma', email => 'selma@google.com' }); }, + check_isa('DB::Exception::ParametersNeeded'), + 'addUser: wrong user_info sent' +); # Check that adding an invalid field ignores that field. @@ -153,18 +149,14 @@ cleanUndef($selma); # cleanup params for comparison: invalid_field dropped, is_admin matches default delete $selma_params->{invalid_field}; $selma_params->{is_admin} = false; -is_deeply($selma_params, $selma, 'addUser: pass in an invalid field'); +is($selma, $selma_params, 'addUser: pass in an invalid field'); # Add a user with an invalid username -throws_ok { - $users_rs->addGlobalUser( - params => { - username => 'my name is selma', - email => 'selma@google.com' - } - ); -} -'DB::Exception::InvalidParameter', 'addUser: bad username sent'; +is( + dies { $users_rs->addGlobalUser(params => { username => 'my name is selma', email => 'selma@google.com' }); }, + check_isa('DB::Exception::InvalidParameter'), + 'addUser: bad username sent' +); # Check that using an email address for a username is valid: # Add one user @@ -179,82 +171,81 @@ my $user2 = { my $added_user2 = $users_rs->addGlobalUser(params => $user2); removeIDs($added_user2); -is_deeply($user2, $added_user2, 'addUser: check that using an email for a username is valid.'); +is($added_user2, $user2, 'addUser: check that using an email for a username is valid.'); # Update a user my $updated_user = clone $user; $updated_user->{email} = 'spring.cop@gmail.com'; -my $up_user_from_db = $users_rs->updateGlobalUser( +my $updated_user_from_db = $users_rs->updateGlobalUser( info => { username => $updated_user->{username} }, params => $updated_user ); -removeIDs($up_user_from_db); -is_deeply($updated_user, $up_user_from_db, 'updateUser: updating a user'); +removeIDs($updated_user_from_db); +is($updated_user_from_db, $updated_user, 'updateUser: updating a user'); # Try to update a user without passing username info -throws_ok { - $users_rs->updateGlobalUser(info => { username_name => 'wiggam' }, params => $updated_user); -} -'DB::Exception::ParametersNeeded', 'updateUser: wrong user_info sent'; +is( + dies { $users_rs->updateGlobalUser(info => { username_name => 'wiggam' }, params => $updated_user); }, + check_isa('DB::Exception::ParametersNeeded'), + 'updateUser: wrong user_info sent' +); # Try to update a user that doesn't exist -throws_ok { - $users_rs->updateGlobalUser(info => { username => 'non_existent_user' }, params => $updated_user); -} -'DB::Exception::UserNotFound', 'updateUser: update user for a non-existing username'; +is( + dies { $users_rs->updateGlobalUser(info => { username => 'non_existent_user' }, params => $updated_user); }, + check_isa('DB::Exception::UserNotFound'), + 'updateUser: update user for a non-existing username' +); -throws_ok { - $users_rs->updateGlobalUser(info => { user_id => -5 }, params => $updated_user); -} -'DB::Exception::UserNotFound', 'updateUser: update user for a non-existing user_id'; +is( + dies { $users_rs->updateGlobalUser(info => { user_id => -5 }, params => $updated_user); }, + check_isa('DB::Exception::UserNotFound'), + 'updateUser: update user for a non-existing user_id' +); # Check that updated an invalid field throws an error -throws_ok { - $users_rs->updateGlobalUser( - info => { - username => 'wiggam' - }, - params => { - invalid_field => 1 - } - ) -} -qr/No such column 'invalid_field'/, 'updateUser: pass in an invalid field'; +like( + dies { $users_rs->updateGlobalUser(info => { username => 'wiggam' }, params => { invalid_field => 1 }) }, + qr/No such column 'invalid_field'/, + 'updateUser: pass in an invalid field' +); # Delete users that were created my $user_to_delete = $users_rs->deleteGlobalUser(info => { username => $user->{username} }); removeIDs($user_to_delete); cleanUndef($user_to_delete); -is_deeply($updated_user, $user_to_delete, 'deleteUser: delete a user'); +is($user_to_delete, $updated_user, 'deleteUser: delete a user'); my $deleted_selma = $users_rs->deleteGlobalUser(info => { username => 'selma' }); removeIDs($deleted_selma); cleanUndef($deleted_selma); -is_deeply($deleted_selma, $selma_params, 'deleteUser: deleter another user'); +is($deleted_selma, $selma_params, 'deleteUser: deleter another user'); my $deleted_patty = $users_rs->deleteGlobalUser(info => { username => 'patty' }); removeIDs($deleted_patty); cleanUndef($deleted_patty); -is_deeply($deleted_patty, $patty_params, 'deleteUser: deleter a third user'); +is($deleted_patty, $patty_params, 'deleteUser: deleter a third user'); my $user_to_delete2 = $users_rs->deleteGlobalUser(info => { username => $added_user2->{username} }); removeIDs($user_to_delete2); cleanUndef($user_to_delete2); -is_deeply($added_user2, $user_to_delete2, 'deleteUser: delete yet another user.'); +is($user_to_delete2, $added_user2, 'deleteUser: delete yet another user.'); # Delete a user that doesn't exist. -throws_ok { - $user = $users_rs->deleteGlobalUser(info => { username => 'undefined_username' }); -} -'DB::Exception::UserNotFound', 'deleteUser: trying to delete with undefined username'; +is( + dies { $user = $users_rs->deleteGlobalUser(info => { username => 'undefined_username' }); }, + check_isa('DB::Exception::UserNotFound'), + 'deleteUser: trying to delete with undefined username' +); -throws_ok { - $user = $users_rs->deleteGlobalUser(info => { user_id => -3 }); -} -'DB::Exception::UserNotFound', 'deleteUser: trying to delete with undefined user_id'; +is( + dies { $user = $users_rs->deleteGlobalUser(info => { user_id => -3 }); }, + check_isa('DB::Exception::UserNotFound'), + 'deleteUser: trying to delete with undefined user_id' +); -## get a list of courses for a user +# get a list of courses for a user my @user_courses = $course_rs->getUserCourses(info => { username => 'lisa' }); for my $user_course (@user_courses) { @@ -288,14 +279,15 @@ for my $user_course (@user_courses_from_csv) { @user_courses_from_csv = sort { $a->{course_name} cmp $b->{course_name} } @user_courses_from_csv; @user_courses = sort { $a->{course_name} cmp $b->{course_name} } @user_courses; -is_deeply(\@user_courses, \@user_courses_from_csv, 'getUserCourses: get all courses for a given user'); +is(\@user_courses, \@user_courses_from_csv, 'getUserCourses: get all courses for a given user'); -## try to get a list of course from a non-existent user +# try to get a list of course from a non-existent user -throws_ok { - $course_rs->getUserCourses(info => { username => 'non_existent_user' }); -} -'DB::Exception::UserNotFound', 'getUserCourses: try to get a list of courses for a non-existent user'; +is( + dies { $course_rs->getUserCourses(info => { username => 'non_existent_user' }); }, + check_isa('DB::Exception::UserNotFound'), + 'getUserCourses: try to get a list of courses for a non-existent user' +); # Check that the users db table is returned to its original state. @users_from_db = $users_rs->getAllGlobalUsers; @@ -304,7 +296,6 @@ for my $user (@users_from_db) { cleanUndef($user); } @users_from_db = sort { $a->{username} cmp $b->{username} } @users_from_db; -is_deeply(\@all_students, \@users_from_db, - 'check: make sure that the users db table is returned to its original state'); +is(\@users_from_db, \@all_students, 'check: make sure that the users db table is returned to its original state'); done_testing; diff --git a/t/db/004_course_users.t b/t/db/004_course_users.t index 0aca1109..79e5bdb4 100644 --- a/t/db/004_course_users.t +++ b/t/db/004_course_users.t @@ -14,15 +14,13 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; +use Test2::V0; use YAML::XS qw/LoadFile/; use Clone qw/clone/; -use Mojo::JSON qw/true false/; +use Mojo::JSON qw/false/; use DB::Schema; use TestUtils qw/loadCSV removeIDs/; -use DB::Utils qw/removeLoginParams/; # Load the database my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; @@ -60,25 +58,28 @@ my @precalc_users_from_db = $user_rs->getCourseUsers(info => { course_name => 'P @precalc_users_from_db = sort { $a->{username} cmp $b->{username} } @precalc_users_from_db; removeIDs($_) for @precalc_users_from_db; -is_deeply(\@precalc_students, \@precalc_users_from_db, 'getUsers: get users from a course'); +is(\@precalc_users_from_db, \@precalc_students, 'getUsers: get users from a course'); # getUsers: Test that an unknown course results in an error. -throws_ok { - $user_rs->getCourseUsers(info => { course_name => 'unknown_course' }); -} -'DB::Exception::CourseNotFound', 'getUsers: undefined course_name'; +is( + dies { $user_rs->getCourseUsers(info => { course_name => 'unknown_course' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getUsers: undefined course_name' +); # getUsers: Test that an unknown course_id results in an error. -throws_ok { - $user_rs->getCourseUsers(info => { course_id => -3 }); -} -'DB::Exception::CourseNotFound', 'getUsers: undefined course_id'; +is( + dies { $user_rs->getCourseUsers(info => { course_id => -3 }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getUsers: undefined course_id' +); # getUsers: Test that not passing either a course_id or course_name results in an error. -throws_ok { - $user_rs->getCourseUsers(info => { my_course => 'Precalculus' }); -} -'DB::Exception::ParametersNeeded', 'getUsers: course_name or course_id not passed in'; +is( + dies { $user_rs->getCourseUsers(info => { my_course => 'Precalculus' }); }, + check_isa('DB::Exception::ParametersNeeded'), + 'getUsers: course_name or course_id not passed in' +); # Test getUser @@ -91,25 +92,28 @@ my $user = $user_rs->getCourseUser( ); removeIDs($user); -is_deeply($precalc_students[0], $user, 'getCourseUser: get one merged user'); +is($user, $precalc_students[0], 'getCourseUser: get one merged user'); # getUser: Test that an unknown course results in an error -throws_ok { - $user_rs->getCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); -} -'DB::Exception::CourseNotFound', 'getCourseUser: undefined course'; +is( + dies { $user_rs->getCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getCourseUser: undefined course' +); # getUser: Test that an unknown user results in an error -throws_ok { - $user_rs->getCourseUser(info => { course_name => 'Precalculus', username => 'unknown_user' }); -} -'DB::Exception::UserNotFound', 'getCourseUser: undefined user'; +is( + dies { $user_rs->getCourseUser(info => { course_name => 'Precalculus', username => 'unknown_user' }); }, + check_isa('DB::Exception::UserNotFound'), + 'getCourseUser: undefined user' +); # getUser: Test that an existing user who is not in the course returns an error. -throws_ok { - $user_rs->getCourseUser(info => { course_name => 'Arithmetic', username => 'marge' }); -} -'DB::Exception::UserNotInCourse', 'getCourseUser: get a user that is not in the course'; +is( + dies { $user_rs->getCourseUser(info => { course_name => 'Arithmetic', username => 'marge' }); }, + check_isa('DB::Exception::UserNotInCourse'), + 'getCourseUser: get a user that is not in the course' +); # addUser: Add a user to a course # Remove the following user if already defined in the course @@ -147,7 +151,7 @@ for my $key (qw/username course_name/) { removeIDs($user); delete $user_params->{course_name}; -is_deeply($course_user_params, $user, 'addCourseUser: add a user to a course'); +is($user, $course_user_params, 'addCourseUser: add a user to a course'); # Check that adding a user returns a merged user. my $quimby_db = $user_rs->addCourseUser( @@ -161,90 +165,78 @@ my $quimby_params = clone($course_user_params); for my $key (keys %$user_params) { $quimby_params->{$key} = $user_params->{$key}; } -is_deeply($quimby_params, $quimby_db, 'addCourseUser: check that an added user is returned merged'); +is($quimby_db, $quimby_params, 'addCourseUser: check that an added user is returned merged'); # Checking that if the course exists, but the user is already a member an exception is thrown. -throws_ok { - $user_rs->addCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); -} -'DB::Exception::CourseNotFound', "addCourseUser: the course doesn't exist"; +is( + dies { $user_rs->addCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); }, + check_isa('DB::Exception::CourseNotFound'), + "addCourseUser: the course doesn't exist" +); # updateUser:Check that the user updates. my $updated_user = { params => { comment => 'Mayor Joe is the best!!' }, recitation => '2' }; -throws_ok { - $user_rs->addCourseUser(info => { course_name => 'Arithmetic', username => 'moe' }); -} -'DB::Exception::UserAlreadyInCourse', 'addCourseUser: the user is already a member'; +is( + dies { $user_rs->addCourseUser(info => { course_name => 'Arithmetic', username => 'moe' }); }, + check_isa('DB::Exception::UserAlreadyInCourse'), + 'addCourseUser: the user is already a member' +); # try to add a non-existent user from a course: -throws_ok { - $user_rs->addCourseUser(info => { course_name => 'Arithmetic', username => 'non_existent_user' }) -} -'DB::Exception::UserNotFound', 'addCourseUser: try to add a non-existent user to a course'; +is( + dies { $user_rs->addCourseUser(info => { course_name => 'Arithmetic', username => 'non_existent_user' }) }, + check_isa('DB::Exception::UserNotFound'), + 'addCourseUser: try to add a non-existent user to a course' +); # addCourseUser: add a user with undefined parameters -throws_ok { - $user_rs->addCourseUser( - info => { - course_name => 'Topology', - username => 'quimby', - }, - params => { - role => 'student', - undefined_field => 1 - } - ); -} -'DBIx::Class::Exception', 'addCourseUser: an undefined field is passed in'; +is( + dies { + $user_rs->addCourseUser( + info => { course_name => 'Topology', username => 'quimby', }, + params => { role => 'student', undefined_field => 1 } + ); + }, + check_isa('DBIx::Class::Exception'), + 'addCourseUser: an undefined field is passed in' +); # Add a user with undefined course user parameters. -throws_ok { - $user_rs->addCourseUser( - info => { - course_name => 'Topology', - username => 'quimby' - }, - params => { - role => 'student', - course_user_params => { - this_is_not_valid => 1 - } - } - ); -} -'DB::Exception::InvalidField', 'addCourseUser: an undefined parameter is set'; +is( + dies { + $user_rs->addCourseUser( + info => { course_name => 'Topology', username => 'quimby' }, + params => { role => 'student', course_user_params => { this_is_not_valid => 1 } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'addCourseUser: an undefined parameter is set' +); # Add a user with nonvalid fields -throws_ok { - $user_rs->addCourseUser( - info => { - course_name => 'Topology', - username => 'quimby' - }, - params => { - role => 'student', - course_user_params => { - useMathQuill => 0 - } - } - ); -} -'DB::Exception::InvalidParameter', 'addCourseUser: an parameter with invalid value'; +is( + dies { + $user_rs->addCourseUser( + info => { course_name => 'Topology', username => 'quimby' }, + params => { role => 'student', course_user_params => { useMathQuill => 0 } } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addCourseUser: an parameter with invalid value' +); # Add a user with an invalid role -throws_ok { - $user_rs->addCourseUser( - info => { - course_name => 'Topology', - username => 'quimby' - }, - params => { - role => 'cop' - } - ); -} -'DB::Exception::UserRoleUndefined', 'addCourseUser: try to add a user with an undefined user role'; +is( + dies { + $user_rs->addCourseUser( + info => { course_name => 'Topology', username => 'quimby' }, + params => { role => 'cop' } + ); + }, + check_isa('DB::Exception::UserRoleUndefined'), + 'addCourseUser: try to add a user with an undefined user role' +); # updateCourseUser: check that the user updates. $updated_user = { @@ -264,69 +256,79 @@ my $user_from_db = $user_rs->updateCourseUser( ); removeIDs($user_from_db); -is_deeply($course_user_params, $user_from_db, 'updateCourseUser: update a single user in an existing course.'); +is($user_from_db, $course_user_params, 'updateCourseUser: update a single user in an existing course.'); # updateCourseUser: check that if the course doesn't exist, an error is thrown: -throws_ok { - $user_rs->updateCourseUser( - info => { course_name => 'unknown_course', username => 'barney' }, - params => $updated_user - ); -} -'DB::Exception::CourseNotFound', "updateCourseUser: the course doesn't exist"; +is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'unknown_course', username => 'barney' }, + params => $updated_user + ); + }, + check_isa('DB::Exception::CourseNotFound'), + "updateCourseUser: the course doesn't exist" +); # updateCourseUser: check that if the course exists, but the user not a member. -throws_ok { - $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', username => 'marge' }, - params => $updated_user - ); -} -'DB::Exception::UserNotInCourse', 'updateCourseUser: the user is not a member of the course'; +is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'Arithmetic', username => 'marge' }, + params => $updated_user + ); + }, + check_isa('DB::Exception::UserNotInCourse'), + 'updateCourseUser: the user is not a member of the course' +); # Try to add a non-existent user from a course. -throws_ok { - $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', user_name => 'bart' }, - params => $updated_user - ); -} -'DB::Exception::ParametersNeeded', 'updateCourseUser: the incorrect information is passed in.'; +is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'Arithmetic', user_name => 'bart' }, + params => $updated_user + ); + }, + check_isa('DB::Exception::ParametersNeeded'), + 'updateCourseUser: the incorrect information is passed in.' +); # Check that a non-existent course throws an error. -throws_ok { - $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', username => 'quimby' }, - params => { sleeps_in_class => 1 } - ); -} -'DBIx::Class::Exception', 'updateCourseUser: an invalid field is set'; +is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'Arithmetic', username => 'quimby' }, + params => { sleeps_in_class => 1 } + ); + }, + check_isa('DBIx::Class::Exception'), + 'updateCourseUser: an invalid field is set' +); # updateCourseUser: update a user with undefined parameters -throws_ok { - $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', username => 'quimby' }, - params => { - course_user_params => { - this_is_not_valid => 1 - } - } - ); -} -'DB::Exception::InvalidField', 'updateCourseUser: an undefined parameter is set'; +is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'Arithmetic', username => 'quimby' }, + params => { course_user_params => { this_is_not_valid => 1 } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'updateCourseUser: an undefined parameter is set' +); # Check that updating a user with nonvalid fields throws an error. -throws_ok { - $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', username => 'quimby' }, - params => { - course_user_params => { - useMathQuill => 'yes' - } - } - ); -} -'DB::Exception::InvalidParameter', 'updateCourseUser: an parameter with invalid value'; +is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'Arithmetic', username => 'quimby' }, + params => { course_user_params => { useMathQuill => 'yes' } } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'updateCourseUser: an parameter with invalid value' +); # Delete a single user from a course. my $deleted_user; @@ -338,30 +340,33 @@ SKIP: { my $deleted_course_user = $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username => 'quimby' }); removeIDs($deleted_course_user); - is_deeply($course_user_params, $deleted_course_user, 'deleteCourseUser: delete a user from a course'); + is($deleted_course_user, $course_user_params, 'deleteCourseUser: delete a user from a course'); $deleted_user = $user_rs->deleteGlobalUser(info => { username => 'quimby' }); removeIDs($deleted_user); - is_deeply($user_params, $deleted_user, 'deleteGlobalUser: delete a user'); + is($deleted_user, $user_params, 'deleteGlobalUser: delete a user'); # deleteUser: Check that if the course doesn't exist, an error is thrown: - throws_ok { - $user_rs->deleteCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); - } - 'DB::Exception::CourseNotFound', "deleteUser: the course doesn't exist"; + is( + dies { $user_rs->deleteCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); }, + check_isa('DB::Exception::CourseNotFound'), + "deleteUser: the course doesn't exist" + ); # deleteUser: Check that if the course exists, but the user not a member. - throws_ok { - $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username => 'marge' }); - } - 'DB::Exception::UserNotInCourse', 'deleteUser: the user is not a member of the course'; + is( + dies { $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username => 'marge' }); }, + check_isa('DB::Exception::UserNotInCourse'), + 'deleteUser: the user is not a member of the course' + ); # deleteUser: Send in username_name instead of username - throws_ok { - $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username_name => 'bart' }); - } - 'DB::Exception::ParametersNeeded', 'deleteUser: the incorrect information is passed in.'; + is( + dies { $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username_name => 'bart' }); }, + check_isa('DB::Exception::ParametersNeeded'), + 'deleteUser: the incorrect information is passed in.' + ); } # Check that the precalc users have not changed. @@ -370,7 +375,6 @@ SKIP: { @precalc_users_from_db = sort { $a->{username} cmp $b->{username} } @precalc_users_from_db; for (@precalc_users_from_db) { removeIDs($_); } -is_deeply(\@precalc_students, \@precalc_users_from_db, - 'check: ensure that the precalc users in the database is restored.'); +is(\@precalc_users_from_db, \@precalc_students, 'check: ensure that the precalc users in the database is restored.'); done_testing; diff --git a/t/db/005_hwsets.t b/t/db/005_hwsets.t index ef79c073..357960d8 100644 --- a/t/db/005_hwsets.t +++ b/t/db/005_hwsets.t @@ -14,11 +14,9 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; +use Test2::V0; use Clone qw/clone/; use YAML::XS qw/LoadFile/; -use DateTime::Format::Strptime; use Mojo::JSON qw/true false/; use DB::Schema; @@ -35,8 +33,6 @@ my $schema = DB::Schema->connect( { quote_names => 1 } ); -my $strp = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); - # $schema->storage->debug(1); # print out the SQL commands. my $problem_set_rs = $schema->resultset('ProblemSet'); @@ -102,7 +98,7 @@ for my $set (@problem_sets_from_db) { delete $set->{course_dates}; } -is_deeply(\@all_problem_sets, \@problem_sets_from_db, 'getProblemSets: get all sets'); +is(\@problem_sets_from_db, \@all_problem_sets, 'getProblemSets: get all sets'); # Filter the precalculus sets: my @precalc_sets = filterBySetType(\@all_problem_sets, undef, 'Precalculus'); @@ -125,7 +121,7 @@ for my $set (@precalc_sets_from_db) { removeIDs($set); } -is_deeply(\@all_precalc_sets, \@precalc_sets_from_db, 'getProblemSets: get sets for one course'); +is(\@precalc_sets_from_db, \@all_precalc_sets, 'getProblemSets: get sets for one course'); # Test all HW sets in one course my @precalc_hw = filterBySetType(\@all_problem_sets, 'HW', 'Precalculus'); @@ -139,26 +135,30 @@ my @precalc_hw_from_db = $problem_set_rs->getHWSets(info => { course_name => 'Pr for my $set (@precalc_hw_from_db) { removeIDs($set); } -is_deeply(\@precalc_hw, \@precalc_hw_from_db, 'getHWSets: get all homework for one course'); +is(\@precalc_hw_from_db, \@precalc_hw, 'getHWSets: get all homework for one course'); # Get one Problem set my $set_one = $precalc_hw[0]; my $set_from_db = $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => $set_one->{set_name} }); removeIDs($set_from_db); -is_deeply($set_one, $set_from_db, 'getProblemSet: get one homework'); +is($set_from_db, $set_one, 'getProblemSet: get one homework'); # Get a problem set that doesn't exist. -throws_ok { - $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'nonexistent_set' }); -} -'DB::Exception::SetNotInCourse', 'getProblemSet: non-existent set name'; +is( + dies { + $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'nonexistent_set' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'getProblemSet: non-existent set name' +); # Try to get a problem set that is not in a given course -throws_ok { - $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_id => 7 }); -} -'DB::Exception::SetNotInCourse', 'getProblemSet: find a set that is not in a course'; +is( + dies { $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_id => 7 }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'getProblemSet: find a set that is not in a course' +); # Add a new problem set my $new_set_params = { @@ -185,7 +185,7 @@ removeIDs($new_set); delete $new_set->{type}; # add the default set_visible $new_set_params->{set_visible} = false; -is_deeply($new_set_params, $new_set, "addProblemSet: add one homework"); +is($new_set, $new_set_params, "addProblemSet: add one homework"); # Try to add a homework without set_name my $new_set2 = { @@ -193,15 +193,11 @@ my $new_set2 = { set_dates => { open => 100, due => 140, answer => 200 }, set_type => 'HW' }; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set2 - } - ); -} -'DB::Exception::ParametersNeeded', 'addProblemSet: set_name not passed in.'; +is( + dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set2 }); }, + check_isa('DB::Exception::ParametersNeeded'), + 'addProblemSet: set_name not passed in.' +); # Try to add a homework with bad date fields my $new_set3 = { @@ -209,15 +205,11 @@ my $new_set3 = { set_dates => { open_set => 100, due => 140, answer => 200 }, set_type => 'HW' }; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set3 - } - ); -} -'DB::Exception::InvalidField', 'addProblemSet: invalid date field passed in.'; +is( + dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set3 }); }, + check_isa('DB::Exception::InvalidField'), + 'addProblemSet: invalid date field passed in.' +); # Try to add a homework set without all required date fields my $new_set4 = { @@ -225,15 +217,11 @@ my $new_set4 = { set_dates => { open => 100, due => 140 }, set_type => 'HW' }; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set4 - } - ); -} -'DB::Exception::FieldsNeeded', 'addProblemSet: missing required date fields'; +is( + dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set4 }); }, + check_isa('DB::Exception::FieldsNeeded'), + 'addProblemSet: missing required date fields' +); # Try to add a homework set without all required date fields my $new_set5 = { @@ -241,15 +229,11 @@ my $new_set5 = { set_dates => { open => 100, due => 140, answer => '1234s' }, set_type => 'HW' }; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set5 - } - ); -} -'DB::Exception::InvalidParameter', 'addProblemSet: adding a non-numeric date'; +is( + dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set5 }); }, + check_isa('DB::Exception::InvalidParameter'), + 'addProblemSet: adding a non-numeric date' +); # Try to add a homework set without invalid date order my $new_set6 = { @@ -258,15 +242,11 @@ my $new_set6 = { set_type => 'HW', set_params => {} }; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set6 - } - ); -} -'DB::Exception::ImproperDateOrder', 'addProblemSet: adding an illegal date order.'; +is( + dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set6 }); }, + check_isa('DB::Exception::ImproperDateOrder'), + 'addProblemSet: adding an illegal date order.' +); # Check for undefined parameter fields my $new_set7 = { @@ -275,57 +255,62 @@ my $new_set7 = { set_type => 'HW', set_params => { not_a_valid_field => 5 } }; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set7 - } - ); -} -'DB::Exception::InvalidField', 'addProblemSet: adding an undefined parameter field'; +is( + dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set7 }); }, + check_isa('DB::Exception::InvalidField'), + 'addProblemSet: adding an undefined parameter field' +); # Check for invalid parameter fields (the hide_hint param is a boolean) -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, - set_type => 'HW', - set_params => { hide_hint => 'yes' } - } - ); -} -'DB::Exception::InvalidParameter', 'addProblemSet: adding an non-valid parameter'; +is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, + set_type => 'HW', + set_params => { hide_hint => 'yes' } + } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addProblemSet: adding an non-valid parameter' +); # Check to ensure true/false are passed into the set_params, not 0/1 -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, - set_type => 'HW', - set_params => { hide_hint => 0 } - } - ); -} -'DB::Exception::InvalidParameter', 'addProblemSet: adding an non-valid boolean parameter'; +is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, + set_type => 'HW', + set_params => { hide_hint => 0 } + } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addProblemSet: adding an non-valid boolean parameter' +); # Check to ensure true/false are passed into the enable_reduced_scoring in set_dates, not 0/1 -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => 0 }, - set_type => 'HW', - set_params => { hide_hint => 0 } - } - ); -} -'DB::Exception::InvalidParameter', 'addProblemSet: adding an non-valid boolean parameter in set_dates'; +is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => 0 }, + set_type => 'HW', + set_params => { hide_hint => 0 } + } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addProblemSet: adding an non-valid boolean parameter in set_dates' +); # Update a set $new_set_params->{set_name} = "HW #8"; @@ -346,7 +331,7 @@ my $updated_set = $problem_set_rs->updateProblemSet( ); removeIDs($updated_set); delete $new_set_params->{type}; -is_deeply($new_set_params, $updated_set, 'updateSet: change the set parameters'); +is($updated_set, $new_set_params, 'updateSet: change the set parameters'); # Update the set where the set_type is sent, but the type is not: $new_set_params->{set_name} = 'HW #88'; @@ -359,7 +344,7 @@ $updated_set = $problem_set_rs->updateProblemSet( ); removeIDs($updated_set); -is_deeply($new_set_params, $updated_set, "updateSet: update a set with set_type defined."); +is($updated_set, $new_set_params, "updateSet: update a set with set_type defined."); # Change the type of a problem set from a Homework Set to a Quiz. @@ -374,66 +359,62 @@ my $set_with_new_type = $problem_set_rs->updateProblemSet( ); removeIDs($set_with_new_type); -is_deeply($set_with_new_type, $set_with_new_type_params, 'updateSet: change the type of the problem set'); +is($set_with_new_type, $set_with_new_type_params, 'updateSet: change the type of the problem set'); # Try to update a set with an illegal field -throws_ok { - $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_id => $new_set_id }, - params => { bad_field => 0 } - ); -} -'DBIx::Class::Exception', 'updateProblemSet: use a non-existing field'; +is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_id => $new_set_id }, + params => { bad_field => 0 } + ); + }, + check_isa('DBIx::Class::Exception'), + 'updateProblemSet: use a non-existing field' +); # Try to update a set with an illegal date field -throws_ok { - $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_id => $new_set_id }, - params => { set_dates => { bad_date => 99 } } - ); -} -'DB::Exception::InvalidField', 'updateSet: invalid date field passed in.'; +is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_id => $new_set_id }, + params => { set_dates => { bad_date => 99 } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'updateSet: invalid date field passed in.' +); # Try to update a set with an dates in a bad order -throws_ok { - $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_id => $new_set_id }, - params => { - set_dates => { - open => 999, - answer => 100 - } - } - ); -} -'DB::Exception::ImproperDateOrder', 'updateSet: adding an illegal date order.'; +is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_id => $new_set_id }, + params => { set_dates => { open => 999, answer => 100 } } + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'updateSet: adding an illegal date order.' +); # Delete a set my $deleted_set = $problem_set_rs->deleteProblemSet(info => { course_name => 'Precalculus', set_name => 'HW #88' }); removeIDs($deleted_set); -is_deeply($set_with_new_type_params, $deleted_set, 'deleteProblemSet: delete a set'); +is($deleted_set, $set_with_new_type_params, 'deleteProblemSet: delete a set'); # Try deleting a set with invalid course_name -throws_ok { - $problem_set_rs->deleteProblemSet( - info => { - course_name => 'Not a course', - set_name => 'HW #1' - } - ); -} -'DB::Exception::CourseNotFound', 'deleteCourse: try to delete a set from a not existent course.'; +is( + dies { $problem_set_rs->deleteProblemSet(info => { course_name => 'Not a course', set_name => 'HW #1' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteCourse: try to delete a set from a not existent course.' +); # Try deleting a set that does not exist -throws_ok { - $problem_set_rs->deleteProblemSet( - info => { - course_name => 'Precalculus', - set_name => 'HW #99' - } - ); -} -'DB::Exception::SetNotInCourse', 'deleteCourse: try to delete a set that not exist.'; +is( + dies { $problem_set_rs->deleteProblemSet(info => { course_name => 'Precalculus', set_name => 'HW #99' }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'deleteCourse: try to delete a set that not exist.' +); # ensure that the problem_sets table in the database is restored. @all_problem_sets = (@hw_sets, @quizzes, @review_sets); @@ -451,6 +432,6 @@ for my $set (@problem_sets_from_db) { # delete $set->{course_name}; } -is_deeply(\@all_problem_sets, \@problem_sets_from_db, 'check: ensure that the problem_sets table is restored.'); +is(\@problem_sets_from_db, \@all_problem_sets, 'check: ensure that the problem_sets table is restored.'); done_testing; diff --git a/t/db/006_quizzes.t b/t/db/006_quizzes.t index 0a89267f..f921eb03 100644 --- a/t/db/006_quizzes.t +++ b/t/db/006_quizzes.t @@ -14,11 +14,9 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; +use Test2::V0; use YAML::XS qw/LoadFile/; use Clone qw/clone/; -use DateTime::Format::Strptime; use Mojo::JSON qw/true false/; use DB::Schema; @@ -35,8 +33,6 @@ my $schema = DB::Schema->connect( { quote_names => 1 } ); -my $strp = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); - # $schema->storage->debug(1); # print out the SQL commands. my $problem_set_rs = $schema->resultset('ProblemSet'); @@ -72,26 +68,32 @@ my @precalc_quizzes_from_db = $problem_set_rs->getQuizzes(info => { course_name for my $quiz (@precalc_quizzes_from_db) { removeIDs($quiz); } -is_deeply(\@precalc_quizzes, \@precalc_quizzes_from_db, 'getQuizzes: get all quizzes for one course'); +is(\@precalc_quizzes_from_db, \@precalc_quizzes, 'getQuizzes: get all quizzes for one course'); # Get a single quiz my $quiz_from_db = $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'Quiz #1' }); removeIDs($quiz_from_db); my @quiz_from_csv = grep { $_->{set_name} eq 'Quiz #1' } @precalc_quizzes; delete $quiz_from_csv[0]->{type}; -is_deeply($quiz_from_csv[0], $quiz_from_db, 'getQuiz: get one quiz from a single course'); +is($quiz_from_db, $quiz_from_csv[0], 'getQuiz: get one quiz from a single course'); # Try to get a quiz that doesn't exist in a course that does. -throws_ok { - $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'nonexisent quiz' }); -} -'DB::Exception::SetNotInCourse', 'getQuiz: non-existent set name'; +is( + dies { + $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'nonexisent quiz' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'getQuiz: non-existent set name' +); # Try to get a quiz from a course that doesn't exist. -throws_ok { - $problem_set_rs->getProblemSet(info => { course_name => 'nonexistent course', set_name => 'Quiz #1' }); -} -'DB::Exception::CourseNotFound', 'getQuiz: try to get a quiz from a non-existent course'; +is( + dies { + $problem_set_rs->getProblemSet(info => { course_name => 'nonexistent course', set_name => 'Quiz #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getQuiz: try to get a quiz from a non-existent course' +); # Add a new quiz my $new_quiz_params = { @@ -109,141 +111,124 @@ my $new_quiz = $problem_set_rs->addProblemSet( ); removeIDs($new_quiz); -## add the default set_visible field +# add the default set_visible field $new_quiz_params->{set_visible} = false; -is_deeply($new_quiz, $new_quiz_params, "addQuiz: add a new quiz"); +is($new_quiz, $new_quiz_params, "addQuiz: add a new quiz"); # Try to add a quiz to a non existent course. -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'nonexistent course', - set_name => 'Quiz #1' - } - ); -} -'DB::Exception::CourseNotFound', 'addQuiz: try to add a quiz from a non-existent course'; +is( + dies { + $problem_set_rs->addProblemSet(params => { course_name => 'nonexistent course', set_name => 'Quiz #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'addQuiz: try to add a quiz from a non-existent course' +); # Try to add a quiz with non-valid parameters. -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_name => 'Quiz #99', - nonexistent_field => 1, - } - ) -} -'DBIx::Class::Exception', 'addQuiz: try to add a quiz with a bad parameter'; +is( + dies { + $problem_set_rs->addProblemSet(params => + { course_name => 'Precalculus', set_type => 'QUIZ', set_name => 'Quiz #99', nonexistent_field => 1, }) + }, + check_isa('DBIx::Class::Exception'), + 'addQuiz: try to add a quiz with a bad parameter' +); # Try to add a quiz without specifying the name. -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_visible => true, - } - ); -} -'DB::Exception::ParametersNeeded', 'addQuiz: try to add a quiz with a bad field'; +is( + dies { + $problem_set_rs->addProblemSet( + params => { course_name => 'Precalculus', set_type => 'QUIZ', set_visible => true, }); + }, + check_isa('DB::Exception::ParametersNeeded'), + 'addQuiz: try to add a quiz with a bad field' +); # Try to add a quiz with an undefined parameter. -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_name => 'Quiz #99', - set_visible => true, - set_params => { - param1 => 0 - }, - set_dates => { - open => 10, - due => 100, - answer => 200, +is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_type => 'QUIZ', + set_name => 'Quiz #99', + set_visible => true, + set_params => { param1 => 0 }, + set_dates => { open => 10, due => 100, answer => 200, } } - } - ); -} -'DB::Exception::InvalidField', 'addQuiz: try to add a quiz with a undefined parameter'; + ); + }, + check_isa('DB::Exception::InvalidField'), + 'addQuiz: try to add a quiz with a undefined parameter' +); # Try to add a quiz with a non-valid parameter. -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_name => 'Quiz #99', - set_visible => true, - set_params => { - timed => 'yes' - }, - set_dates => { - open => 10, - due => 100, - answer => 200, +is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_type => 'QUIZ', + set_name => 'Quiz #99', + set_visible => true, + set_params => { timed => 'yes' }, + set_dates => { open => 10, due => 100, answer => 200, } } - } - ); -} -'DB::Exception::InvalidParameter', 'addQuiz: try to add a quiz with a non-valid parameter'; + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addQuiz: try to add a quiz with a non-valid parameter' +); # Try to add a quiz with a missing required date fields. -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_name => 'Quiz #99', - set_dates => { - open => 10, - due => 100 +is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_type => 'QUIZ', + set_name => 'Quiz #99', + set_dates => { open => 10, due => 100 } } - } - ); -} -'DB::Exception::FieldsNeeded', 'addQuiz: try to add a quiz with a missing required date fields'; + ); + }, + check_isa('DB::Exception::FieldsNeeded'), + 'addQuiz: try to add a quiz with a missing required date fields' +); # Try to add a quiz with an undefined date field. -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_name => 'Quiz #99', - set_visible => 1, - set_dates => { - open => 10, - due => 100, - answer => 200, - reduced_scoring => 300 +is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_type => 'QUIZ', + set_name => 'Quiz #99', + set_visible => 1, + set_dates => { open => 10, due => 100, answer => 200, reduced_scoring => 300 } } - } - ); -} -'DB::Exception::InvalidField', 'addQuiz: try to add a quiz with an undefined date field'; + ); + }, + check_isa('DB::Exception::InvalidField'), + 'addQuiz: try to add a quiz with an undefined date field' +); # Try to add a quiz with dates that are out of order. -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_name => 'Quiz #99', - set_visible => 1, - set_dates => { - open => 10, - due => 300, - answer => 200, +is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_type => 'QUIZ', + set_name => 'Quiz #99', + set_visible => 1, + set_dates => { open => 10, due => 300, answer => 200, } } - } - ); -} -'DB::Exception::ImproperDateOrder', 'addQuiz: try to add a quiz with dates that are out of order'; - + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'addQuiz: try to add a quiz with dates that are out of order' +); # Update the visibility of the quiz my $updated_params = { set_visible => 0 }; my $updated_quiz = $problem_set_rs->updateProblemSet( @@ -257,15 +242,11 @@ my $updated_quiz = $problem_set_rs->updateProblemSet( $new_quiz->{set_visible} = false; $new_quiz->{set_params} = {}; removeIDs($updated_quiz); -is_deeply($new_quiz, $updated_quiz, 'updateQuiz: successfully update the quiz'); +is($updated_quiz, $new_quiz, 'updateQuiz: successfully update the quiz'); # Update the params of the quiz -$updated_params = { - set_params => { - timed => true - } -}; -$updated_quiz = $problem_set_rs->updateProblemSet( +$updated_params = { set_params => { timed => true } }; +$updated_quiz = $problem_set_rs->updateProblemSet( info => { course_name => 'Precalculus', set_name => 'Quiz #9' @@ -274,7 +255,7 @@ $updated_quiz = $problem_set_rs->updateProblemSet( ); removeIDs($updated_quiz); $new_quiz->{set_params} = { timed => true }; -is_deeply($new_quiz, $updated_quiz, 'updateQuiz: successfully update the params of the quiz'); +is($updated_quiz, $new_quiz, 'updateQuiz: successfully update the params of the quiz'); # Update the dates of the quiz $updated_params = { @@ -293,130 +274,98 @@ $updated_quiz = $problem_set_rs->updateProblemSet( ); removeIDs($updated_quiz); $new_quiz->{set_dates} = clone($updated_params->{set_dates}); -is_deeply($new_quiz, $updated_quiz, 'updateQuiz: successfully update the dates of the quiz'); +is($updated_quiz, $new_quiz, 'updateQuiz: successfully update the dates of the quiz'); # Try to update a non-existent field of the quiz. -throws_ok { - $problem_set_rs->updateProblemSet( - info => { - course_name => 'Precalculus', - set_name => 'Quiz #9' - }, - params => { - nonexistent_field => 1 - } - ); -} -'DBIx::Class::Exception', 'updateQuiz: try to update a quiz with a non-valid field'; +is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => { nonexistent_field => 1 } + ); + }, + check_isa('DBIx::Class::Exception'), + 'updateQuiz: try to update a quiz with a non-valid field' +); # Try to update a non-existent param of the quiz. -throws_ok { - $problem_set_rs->updateProblemSet( - info => { - course_name => 'Precalculus', - set_name => 'Quiz #9' - }, - params => { - set_params => { - show_hint => 1 - } - } - ); -} -'DB::Exception::InvalidField', 'updateQuiz: try to update a quiz with an undefined parameter'; +is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => { set_params => { show_hint => 1 } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'updateQuiz: try to update a quiz with an undefined parameter' +); # Try to update a parameter with a bad value -throws_ok { - $problem_set_rs->updateProblemSet( - info => { - course_name => 'Precalculus', - set_name => 'Quiz #9' - }, - params => { - set_params => { - timed => 'yes' - } - } - ); -} -'DB::Exception::InvalidParameter', 'updateQuiz: try to update a quiz with a non-valid field'; +is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => { set_params => { timed => 'yes' } } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'updateQuiz: try to update a quiz with a non-valid field' +); # Try to update a quiz with an invalid date -throws_ok { - $problem_set_rs->updateProblemSet( - info => { - course_name => 'Precalculus', - set_name => 'Quiz #9' - }, - params => { - set_dates => { - reduced_scoring => 1000 - } - } - ); -} -'DB::Exception::InvalidField', 'updateQuiz: try to update a quiz with a non-valid date'; +is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => { set_dates => { reduced_scoring => 1000 } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'updateQuiz: try to update a quiz with a non-valid date' +); # Try to update a quiz with a date out of order. -throws_ok { - $problem_set_rs->updateProblemSet( - info => { - course_name => 'Precalculus', - set_name => 'Quiz #9' - }, - params => { - set_dates => { - open => 50, - due => 40 - } - } - ); -} -'DB::Exception::ImproperDateOrder', 'updateQuiz: try to update a quiz with out of order dates'; +is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => { set_dates => { open => 50, due => 40 } } + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'updateQuiz: try to update a quiz with out of order dates' +); # Try to delete from a non-existent course. -throws_ok { - $problem_set_rs->deleteProblemSet( - info => { - course_name => 'Course does not exist', - set_name => 'Quiz #9' - } - ); -} -'DB::Exception::CourseNotFound', 'deleteQuiz: try to delete a quiz from a non-existent course'; +is( + dies { + $problem_set_rs->deleteProblemSet( + info => { course_name => 'Course does not exist', set_name => 'Quiz #9' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteQuiz: try to delete a quiz from a non-existent course' +); # Try to delete from a non-existent course. -throws_ok { - $problem_set_rs->deleteProblemSet( - info => { - course_id => 9999, - set_name => 'Quiz #9' - } - ); -} -'DB::Exception::CourseNotFound', 'deleteQuiz: try to delete a quiz from a non-existent course_id'; +is( + dies { $problem_set_rs->deleteProblemSet(info => { course_id => 9999, set_name => 'Quiz #9' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteQuiz: try to delete a quiz from a non-existent course_id' +); # Try to delete from a non-existent set in a course. -throws_ok { - $problem_set_rs->deleteProblemSet( - info => { - course_name => 'Precalculus', - set_name => 'Quiz #999' - } - ); -} -'DB::Exception::SetNotInCourse', 'deleteQuiz: try to delete a non-existent quiz'; +is( + dies { $problem_set_rs->deleteProblemSet(info => { course_name => 'Precalculus', set_name => 'Quiz #999' }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'deleteQuiz: try to delete a non-existent quiz' +); # Try to delete from a non-existent set in a course. -throws_ok { - $problem_set_rs->deleteProblemSet( - info => { - course_name => 'Precalculus', - set_id => 99999 - } - ); -} -'DB::Exception::SetNotInCourse', 'deleteQuiz: try to delete a non-existent quiz as set_id'; +is( + dies { $problem_set_rs->deleteProblemSet(info => { course_name => 'Precalculus', set_id => 99999 }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'deleteQuiz: try to delete a non-existent quiz as set_id' +); # Try to delete from a non-existent set in a course: my $deleted_quiz = $problem_set_rs->deleteProblemSet( @@ -426,7 +375,7 @@ my $deleted_quiz = $problem_set_rs->deleteProblemSet( } ); removeIDs($deleted_quiz); -is_deeply($deleted_quiz, $new_quiz, 'delete Quiz: successfully delete a quiz'); +is($deleted_quiz, $new_quiz, 'delete Quiz: successfully delete a quiz'); # Ensure that the quizzes in the database are restored. @precalc_quizzes_from_db = $problem_set_rs->getQuizzes(info => { course_name => 'Precalculus' }); @@ -435,6 +384,6 @@ is_deeply($deleted_quiz, $new_quiz, 'delete Quiz: successfully delete a quiz'); for my $quiz (@precalc_quizzes_from_db) { removeIDs($quiz); } -is_deeply(\@precalc_quizzes, \@precalc_quizzes_from_db, 'check: ensure that the quizzes have been restored.'); +is(\@precalc_quizzes_from_db, \@precalc_quizzes, 'check: ensure that the quizzes have been restored.'); done_testing; diff --git a/t/db/007_user_set.t b/t/db/007_user_set.t index fb5abff4..ea100fef 100644 --- a/t/db/007_user_set.t +++ b/t/db/007_user_set.t @@ -4,7 +4,6 @@ use warnings; use strict; -use feature 'say'; BEGIN { use File::Basename qw/dirname/; @@ -15,10 +14,8 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use DateTime::Format::Strptime; -use Test::More; +use Test2::V0; use Clone qw/clone/; -use Test::Exception; use YAML::XS qw/LoadFile/; use Mojo::JSON qw/true false/; @@ -39,7 +36,6 @@ my $schema = DB::Schema->connect( ); # $schema->storage->debug(1); # print out the SQL commands. -my $strp = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); my $user_set_rs = $schema->resultset('UserSet'); my $course_rs = $schema->resultset('Course'); @@ -147,7 +143,7 @@ for my $set (@all_user_sets_from_db) { @all_user_sets_from_db = sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @all_user_sets_from_db; -is_deeply(\@all_user_sets_from_db, \@all_user_sets, 'getAllUserSets: get all user sets for all courses'); +is(\@all_user_sets_from_db, \@all_user_sets, 'getAllUserSets: get all user sets for all courses'); my @merged_sets_from_db = $user_set_rs->getAllUserSets(merged => 1); @@ -161,7 +157,7 @@ for my $merged_set (@merged_sets_from_db) { @merged_user_sets = sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @merged_user_sets; -is_deeply(\@merged_sets_from_db, \@merged_user_sets, 'getAllUserSets: get all merged sets for all courses'); +is(\@merged_sets_from_db, \@merged_user_sets, 'getAllUserSets: get all merged sets for all courses'); # Get all user set for a given user in a course. @@ -183,7 +179,7 @@ for my $user_set (@user_sets_from_db) { @user_sets_from_db = sort { $a->{set_name} cmp $b->{set_name} } @user_sets_from_db; @user_sets_for_one_user = sort { $a->{set_name} cmp $b->{set_name} } @user_sets_for_one_user; -is_deeply(\@user_sets_from_db, \@user_sets_for_one_user, 'getUserSets: get all user sets for one user'); +is(\@user_sets_from_db, \@user_sets_for_one_user, 'getUserSets: get all user sets for one user'); # Get all merged sets for a given user in a course @@ -202,9 +198,9 @@ for my $merged_set (@merged_sets_from_db) { removeIDs($merged_set); } -is_deeply(\@merged_sets_from_db, \@merged_sets_for_one_user, 'getUserSets: get all merged sets for one user'); +is(\@merged_sets_from_db, \@merged_sets_for_one_user, 'getUserSets: get all merged sets for one user'); -## get all user sets for a given set in a course +# get all user sets for a given set in a course my @user_sets_for_one_set = grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @all_user_sets; @@ -215,7 +211,7 @@ for my $user_set (@user_sets_from_db) { delete $user_set->{set_visible} unless defined($user_set->{set_visible}); } -is_deeply(\@user_sets_from_db, \@user_sets_for_one_set, 'getUserSets: get all user sets for a set in a course'); +is(\@user_sets_from_db, \@user_sets_for_one_set, 'getUserSets: get all user sets for a set in a course'); # get all merged user sets for a given set in a course @@ -231,25 +227,33 @@ for my $user_set (@merged_sets_from_db) { removeIDs($user_set); } -is_deeply(\@merged_sets_from_db, \@merged_sets_for_one_set, 'getUserSets: get all merged sets for a set in a course'); +is(\@merged_sets_from_db, \@merged_sets_for_one_set, 'getUserSets: get all merged sets for a set in a course'); # Try to get a user set from a non-existing course. -throws_ok { - $user_set_rs->getUserSetsForUser(info => { course_name => 'non_existent_course', username => 'homer' }); -} -'DB::Exception::CourseNotFound', 'getUserSets: attempt to get user sets from a nonexistent course'; +is( + dies { + $user_set_rs->getUserSetsForUser(info => { course_name => 'non_existent_course', username => 'homer' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getUserSets: attempt to get user sets from a nonexistent course' +); # Try to get a user set from a non-existing course. -throws_ok { - $user_set_rs->getUserSetsForUser(info => { course_name => 'Precalculus', username => 'non_existent_user' }); -} -'DB::Exception::UserNotFound', 'getUserSets: attempt to get user sets from a nonexistent user'; +is( + dies { + $user_set_rs->getUserSetsForUser(info => { course_name => 'Precalculus', username => 'non_existent_user' }); + }, + check_isa('DB::Exception::UserNotFound'), + 'getUserSets: attempt to get user sets from a nonexistent user' +); # Try to get a user set from a user not in the course. -throws_ok { - $user_set_rs->getUserSetsForUser(info => { course_name => 'non_existent_course', username => 'bart' }); -} -'DB::Exception::CourseNotFound', 'getUserSets: attempt to get user sets from user not in the course'; +is( + dies { $user_set_rs->getUserSetsForUser(info => { course_name => 'non_existent_course', username => 'bart' }); } + , + check_isa('DB::Exception::CourseNotFound'), + 'getUserSets: attempt to get user sets from user not in the course' +); # Get a single UserSet my $info = { @@ -272,7 +276,7 @@ my $user_set_from_csv = clone( removeIDs($user_set); delete $user_set->{set_visible} unless defined($user_set->{set_visible}); -is_deeply($user_set_from_csv, $user_set, 'getUserSet: get a user set from a course'); +is($user_set, $user_set_from_csv, 'getUserSet: get a user set from a course'); # Get a merged UserSet @@ -289,55 +293,47 @@ my $merged_set_from_csv = clone( my $merged_set = $user_set_rs->getUserSet(info => $info, merged => 1); removeIDs($merged_set); -is_deeply($merged_set_from_csv, $merged_set, 'getUserSet: get a merged set from a course'); +is($merged_set, $merged_set_from_csv, 'getUserSet: get a merged set from a course'); # Try to get a user set from a non-existent course. -throws_ok { - $user_set_rs->getUserSet( - info => { - course_name => 'non_existent_course', - username => 'homer', - set_name => 'HW #1' - } - ); -} -'DB::Exception::CourseNotFound', 'getUserSet: try to get a user set from a non-existent course'; +is( + dies { + $user_set_rs->getUserSet( + info => { course_name => 'non_existent_course', username => 'homer', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getUserSet: try to get a user set from a non-existent course' +); # Try to get a user set from a non-existent user. -throws_ok { - $user_set_rs->getUserSet( - info => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1' - } - ); -} -'DB::Exception::UserNotFound', 'getUserSet: try to get a user set from a non-existent user'; +is( + dies { + $user_set_rs->getUserSet( + info => { course_name => 'Precalculus', username => 'non_existent_user', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::UserNotFound'), + 'getUserSet: try to get a user set from a non-existent user' +); # Try to get a user set from a user not in the course. -throws_ok { - $user_set_rs->getUserSet( - info => { - course_name => 'Precalculus', - username => 'marge', - set_name => 'HW #1' - } - ); -} -'DB::Exception::UserNotInCourse', 'getUserSet: try to get a user set not in the course'; +is( + dies { + $user_set_rs->getUserSet( + info => { course_name => 'Precalculus', username => 'marge', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::UserNotInCourse'), + 'getUserSet: try to get a user set not in the course' +); # Try to get a user set from a non-existent set. -throws_ok { - $user_set_rs->getUserSet( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #999' - } - ); -} -'DB::Exception::SetNotInCourse', 'getUserSet: try to get a user set from a non-existent set'; +is( + dies { + $user_set_rs->getUserSet( + info => { course_name => 'Precalculus', username => 'homer', set_name => 'HW #999' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'getUserSet: try to get a user set from a non-existent set' +); # Add a user set my $new_info = { @@ -356,7 +352,7 @@ $new_info->{set_params} = {}; $new_info->{set_version} = 0; $new_info->{set_type} = 'HW'; -is_deeply($new_user_set, $new_info, 'addUserSet: add a new user set'); +is($new_user_set, $new_info, 'addUserSet: add a new user set'); my $hw_set1 = clone((grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @hw_sets)[0]); @@ -374,7 +370,7 @@ removeIDs($new_merged_set); # add the default value for set_version $hw_set1->{set_version} = 0; -is_deeply($new_merged_set, $hw_set1, 'addUserSet: add a new user set and check that it is merged correctly'); +is($new_merged_set, $hw_set1, 'addUserSet: add a new user set and check that it is merged correctly'); # Add a user set with a empty set of dates. @@ -394,7 +390,7 @@ $new_user_params2->{set_type} = 'HW'; $new_user_params2->{set_version} = 0; $new_user_params2->{set_params} = {}; -is_deeply($new_user_set2, $new_user_params2, 'addUserSet: add a new user set with empty dates.'); +is($new_user_set2, $new_user_params2, 'addUserSet: add a new user set with empty dates.'); # Test that adding a field that is not in the database is ignored. my $user_set_params3 = { @@ -416,55 +412,47 @@ delete $user_set_params3->{bad_field}; # and need to explicitly set the type $user_set_params3->{set_type} = 'HW'; -is_deeply($user_set3, $user_set_params3, 'addUserSet: add a user set with a bad field'); +is($user_set3, $user_set_params3, 'addUserSet: add a user set with a bad field'); # Try to add a user set to a course that doesn't exist. -throws_ok { - $user_set_rs->addUserSet( - params => { - username => 'otto', - course_name => 'non existent course', - set_name => 'HW #1' - } - ); -} -'DB::Exception::CourseNotFound', 'addUserSet: try to add a user set to a non-existent course'; +is( + dies { + $user_set_rs->addUserSet( + params => { username => 'otto', course_name => 'non existent course', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'addUserSet: try to add a user set to a non-existent course' +); # Try to add a user set for a set that does not exist in a course. -throws_ok { - $user_set_rs->addUserSet( - params => { - username => 'otto', - course_name => 'Precalculus', - set_name => 'HW #99' - } - ); -} -'DB::Exception::SetNotInCourse', 'addUserSet: try to add a user set to a non-existent set'; +is( + dies { + $user_set_rs->addUserSet( + params => { username => 'otto', course_name => 'Precalculus', set_name => 'HW #99' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'addUserSet: try to add a user set to a non-existent set' +); # Try to add a user set for a user that is not in a course. -throws_ok { - $user_set_rs->addUserSet( - params => { - username => 'ralph', - course_name => 'Abstract Algebra', - set_name => 'HW #1' - } - ); -} -'DB::Exception::UserNotInCourse', 'addUserSet: try to add a user set for a user who is not in the course'; +is( + dies { + $user_set_rs->addUserSet( + params => { username => 'ralph', course_name => 'Abstract Algebra', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::UserNotInCourse'), + 'addUserSet: try to add a user set for a user who is not in the course' +); # Try to add a user_set that already exists. -throws_ok { - $user_set_rs->addUserSet( - params => { - username => 'otto', - course_name => 'Precalculus', - set_name => 'HW #1' - } - ); -} -'DB::Exception::UserSetExists', 'addUserSet: try to add a user set that already exists'; +is( + dies { + $user_set_rs->addUserSet( + params => { username => 'otto', course_name => 'Precalculus', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::UserSetExists'), + 'addUserSet: try to add a user set that already exists' +); my $otto_set_info2 = { username => 'otto', @@ -492,26 +480,26 @@ $set_params2->{set_params} = { description => 'This is the description for HW # $set_params2->{set_version} = 1; $set_params2->{set_type} = 'HW'; -is_deeply($user_set2, $set_params2, 'addUserSet: add a new user set with params'); +is($user_set2, $set_params2, 'addUserSet: add a new user set with params'); # When adding a user set with a bad field in the params, it is ignored -throws_ok { - $user_set_rs->addUserSet( - params => { - username => 'otto', - course_name => 'Precalculus', - set_name => 'HW #4', - set_version => 1, - set_params => { - bad_field => 12, - hide_hint => false +is( + dies { + $user_set_rs->addUserSet( + params => { + username => 'otto', + course_name => 'Precalculus', + set_name => 'HW #4', + set_version => 1, + set_params => { bad_field => 12, hide_hint => false } } - } - ) -} -'DB::Exception::InvalidField', 'addUserSet: try to add a new user set with an undefined parameter'; + ) + }, + check_isa('DB::Exception::InvalidField'), + 'addUserSet: try to add a new user set with an undefined parameter' +); -## add a user set with a new date +# add a user set with a new date my $set_dates4 = { open => 1, @@ -544,7 +532,7 @@ $set_params4->{set_type} = 'HW'; $set_params4->{set_params} = {}; $set_params4->{set_version} = 1; -is_deeply($user_set4, $set_params4, 'addUserSet: add a new user set with dates'); +is($user_set4, $set_params4, 'addUserSet: add a new user set with dates'); # add a user with only override dates set. @@ -577,38 +565,30 @@ cleanUndef($ralph_user_set); $ralph_set_info->{set_type} = 'HW'; $ralph_set_info->{set_version} = 0; -is_deeply($ralph_user_set, $ralph_set_info, 'addUserSet: add a new user with dates (some are missing).'); +is($ralph_user_set, $ralph_set_info, 'addUserSet: add a new user with dates (some are missing).'); # Try to add a bad date. -throws_ok { - $user_set_rs->addUserSet( - params => { - %$otto_set_info5, - set_dates => { - open => 100, - due => 9, - answer => 1000, - enable_reduced_scoring => false - } - } - ); -} -'DB::Exception::ImproperDateOrder', 'addUserSet: dates are out of order'; - -throws_ok { - $user_set_rs->addUserSet( - params => { - %$otto_set_info5, - set_dates => { - open => 100, - due => 900, - answer => 800 +is( + dies { + $user_set_rs->addUserSet( + params => { + %$otto_set_info5, + set_dates => { open => 100, due => 9, answer => 1000, enable_reduced_scoring => false } } - } - ); -} -'DB::Exception::ImproperDateOrder', 'addUserSet: dates are out of order'; + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'addUserSet: dates are out of order' +); +is( + dies { + $user_set_rs->addUserSet( + params => { %$otto_set_info5, set_dates => { open => 100, due => 900, answer => 800 } }); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'addUserSet: dates are out of order' +); # Add a new user set and test that it is merged correctly. my $otto_quiz_info = { @@ -648,42 +628,29 @@ removeIDs($user_set_to_merge); # otto_quiz has set_version 0. Need to match to compare. $merged_set1->{set_version} = 0; -is_deeply($merged_set1, $user_set_to_merge, 'addUserSet: adding a user set with dates to check merging'); - -## Check that adding a user set that are out of order with the problem sets throws an error. +is($user_set_to_merge, $merged_set1, 'addUserSet: adding a user set with dates to check merging'); -throws_ok { - $user_set_rs->addUserSet( - params => { - %$otto_set_info5, - set_dates => { - due => 1609595640, # this is after the problem set answer date. - } - }, - merged => 1 - ); -} -'DB::Exception::ImproperDateOrder', 'addUserSet: user set is out of order with respect to problem set'; - -## Check that setting a boolean as 0/1 throws an error - -# Currently, this is stripped out if hide_hint is 0. Doe we want to check for this? +# Check that adding a user set that is out of order with the problem sets throws an error. +# This set has a due date after the answer date. +is( + dies { + $user_set_rs->addUserSet(params => { %$otto_set_info5, set_dates => { due => 1609595640 } }, merged => 1); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'addUserSet: user set is out of order with respect to problem set' +); -throws_ok { - $user_set_rs->addUserSet( - params => { - %$otto_set_info5, - set_params => { - hide_hint => 1 - } - }, - merged => 1 - ); -} -'DB::Exception::InvalidParameter', 'addUserSet: boolean valid should be JSON boolean'; +# Check that setting a boolean as 0/1 throws an error +# Currently, this is stripped out if hide_hint is 0. Do we want to check for this? +is( + dies { + $user_set_rs->addUserSet(params => { %$otto_set_info5, set_params => { hide_hint => 1 } }, merged => 1); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addUserSet: boolean valid should be JSON boolean' +); # Update User Set -# Get the user set for $otto_set_info2. my $otto_quiz = $user_set_rs->getUserSet(info => $otto_quiz_info); removeIDs($otto_quiz); @@ -700,7 +667,7 @@ $otto_quiz->{set_dates}->{answer} = $updated_dates->{answer}; my $updated_user_quiz = $user_set_rs->updateUserSet(info => $otto_quiz_info, params => $otto_quiz); removeIDs($updated_user_quiz); -is_deeply($updated_user_quiz, $otto_quiz, 'updateUserSet: update the dates'); +is($updated_user_quiz, $otto_quiz, 'updateUserSet: update the dates'); # Update the params my $updated_user_set2 = $user_set_rs->updateUserSet( @@ -713,7 +680,7 @@ my $updated_user_set2 = $user_set_rs->updateUserSet( ); removeIDs($updated_user_set2); $otto_quiz->{set_params}->{problem_randorder} = true; -is_deeply($updated_user_set2, $otto_quiz, 'updateUserSet: update the params'); +is($updated_user_set2, $otto_quiz, 'updateUserSet: update the params'); # Update a valid field my $updated_user_set3 = $user_set_rs->updateUserSet( @@ -725,68 +692,56 @@ my $updated_user_set3 = $user_set_rs->updateUserSet( removeIDs($updated_user_set3); $otto_quiz->{set_visible} = true; -is_deeply($otto_quiz, $updated_user_set3, 'updateUserSet: update the set visibility'); +is($otto_quiz, $updated_user_set3, 'updateUserSet: update the set visibility'); # Try updating an invalid param. -throws_ok { - $user_set_rs->updateUserSet( - info => $otto_quiz_info, - params => { - set_params => { - not_a_valid_param => 'bad' - } - } - ); -} -'DB::Exception::InvalidField', 'updateUserSet: try setting a parameter that does not exist'; +is( + dies { + $user_set_rs->updateUserSet( + info => $otto_quiz_info, + params => { set_params => { not_a_valid_param => 'bad' } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'updateUserSet: try setting a parameter that does not exist' +); # Try updating an invalid date. -throws_ok { - $user_set_rs->updateUserSet( - info => $otto_quiz_info, - params => { - set_dates => { - open => 1, - closed => 2 - } - } - ); -} -'DB::Exception::InvalidField', 'updateUserSet: try to update an invalid date field'; +is( + dies { + $user_set_rs->updateUserSet(info => $otto_quiz_info, params => { set_dates => { open => 1, closed => 2 } }); + }, + check_isa('DB::Exception::InvalidField'), + 'updateUserSet: try to update an invalid date field' +); # Test with out of order dates. -throws_ok { - $user_set_rs->updateUserSet( - info => $otto_quiz_info, - params => { - set_dates => { - open => 100, - due => 2, - answer => 200 - } - } - ); -} -'DB::Exception::ImproperDateOrder', 'updateUserSet: try to update with out of order dates'; +is( + dies { + $user_set_rs->updateUserSet( + info => $otto_quiz_info, + params => { set_dates => { open => 100, due => 2, answer => 200 } } + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'updateUserSet: try to update with out of order dates' +); # Try to update a user_set that doesn't exist. -throws_ok { - $user_set_rs->updateUserSet( - info => $otto_set_info5, - params => { - set_params => { - hide_hint => true - } - } - ); -} -'DB::Exception::UserSetNotInCourse', 'updateUserSet: try to update a user set not the in the course'; +is( + dies { + $user_set_rs->updateUserSet(info => $otto_set_info5, params => { set_params => { hide_hint => true } }); + }, + check_isa('DB::Exception::UserSetNotInCourse'), + 'updateUserSet: try to update a user set not the in the course' +); # Try to delete a user_set that doesn't exist. -throws_ok { - $user_set_rs->deleteUserSet(info => $otto_set_info5); -} -'DB::Exception::UserSetNotInCourse', 'deleteUserSet: try to delete a user set not the in the course'; +is( + dies { $user_set_rs->deleteUserSet(info => $otto_set_info5); }, + check_isa('DB::Exception::UserSetNotInCourse'), + 'deleteUserSet: try to delete a user set not the in the course' +); # Delete some user sets that were created. @@ -801,7 +756,7 @@ removeIDs($deleted_user_set); cleanUndef($deleted_user_set); removeIDs($new_user_set2); cleanUndef($new_user_set2); -is_deeply($deleted_user_set, $new_user_set2, "deleteUserSet: successfully delete a user set"); +is($deleted_user_set, $new_user_set2, "deleteUserSet: successfully delete a user set"); my $deleted_user_set2 = $user_set_rs->deleteUserSet( info => { @@ -812,12 +767,12 @@ my $deleted_user_set2 = $user_set_rs->deleteUserSet( ); removeIDs($deleted_user_set2); cleanUndef($deleted_user_set2); -is_deeply($deleted_user_set2, $ralph_user_set, "deleteUserSet: successfully delete another user set"); +is($deleted_user_set2, $ralph_user_set, "deleteUserSet: successfully delete another user set"); my $deleted_user_set3 = $user_set_rs->deleteUserSet(info => $otto_quiz_info); removeIDs($deleted_user_set3); cleanUndef($deleted_user_set3); -is_deeply($deleted_user_set3, $otto_quiz, "deleteUserSet: successfully delete yet another user set"); +is($deleted_user_set3, $otto_quiz, "deleteUserSet: successfully delete yet another user set"); my $deleted_user_set4 = $user_set_rs->deleteUserSet(info => $new_merged_set); @@ -856,6 +811,6 @@ for my $set (@all_user_sets_from_db) { @all_user_sets_from_db = sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @all_user_sets_from_db; -is_deeply(\@all_user_sets_from_db, \@all_user_sets, 'check: ensure that the user sets are restored.'); +is(\@all_user_sets_from_db, \@all_user_sets, 'check: ensure that the user sets are restored.'); done_testing; diff --git a/t/db/008_problem_pools.t b/t/db/008_problem_pools.t index dc731a66..a8a72346 100644 --- a/t/db/008_problem_pools.t +++ b/t/db/008_problem_pools.t @@ -14,8 +14,7 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; +use Test2::V0; use YAML::XS qw/LoadFile/; use Clone qw/clone/; @@ -60,7 +59,7 @@ for my $pool (@problem_pools_from_db) { removeIDs($pool); } -is_deeply(\@problem_pools_from_file, \@problem_pools_from_db, 'getAllProblemPools: find all problem pools'); +is(\@problem_pools_from_db, \@problem_pools_from_file, 'getAllProblemPools: find all problem pools'); my @precalc_pools = $problem_pool_rs->getProblemPools(info => { course_name => 'Precalculus' }); @@ -69,7 +68,7 @@ for my $pool (@precalc_pools) { $pool->{course_name} = 'Precalculus'; } -is_deeply(\@precalc_pools_from_file, \@precalc_pools, 'getProblemPools: get all problem pools from a single course'); +is(\@precalc_pools, \@precalc_pools_from_file, 'getProblemPools: get all problem pools from a single course'); # Get a problem pool my $pool_to_fetch = $problem_pools_from_file[0]; @@ -78,19 +77,26 @@ my $fetched_pool = $problem_pool_rs->getProblemPool(info => $pool_to_fetch); removeIDs($fetched_pool); $fetched_pool->{course_name} = $problem_pools_from_file[0]->{course_name}; -is_deeply($pool_to_fetch, $fetched_pool, 'getProblemPool: get a single pool from a course'); +is($fetched_pool, $pool_to_fetch, 'getProblemPool: get a single pool from a course'); # Try to get a problem pool from a course that doesn't exist. -throws_ok { - $problem_pool_rs->getProblemPool(info => { course_name => 'not existent course', pool_name => 'adding fractions' }); -} -'DB::Exception::CourseNotFound', 'getProblemPool: get a problem pool from a non-existent course'; +is( + dies { + $problem_pool_rs->getProblemPool( + info => { course_name => 'not existent course', pool_name => 'adding fractions' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getProblemPool: get a problem pool from a non-existent course' +); # Try to get a problem pool from a course, but the pool doesn't exist. -throws_ok { - $problem_pool_rs->getProblemPool(info => { course_name => 'Arithmetic', pool_name => 'non_existent_pool' }); -} -'DB::Exception::PoolNotInCourse', 'getProblemPool: get a problem pool from a non-existent course'; +is( + dies { + $problem_pool_rs->getProblemPool(info => { course_name => 'Arithmetic', pool_name => 'non_existent_pool' }); + }, + check_isa('DB::Exception::PoolNotInCourse'), + 'getProblemPool: get a problem pool from a non-existent course' +); # Add a problem pool my $course_name = 'Arithmetic'; @@ -104,36 +110,27 @@ my $pool2 = $problem_pool_rs->addProblemPool( ); removeIDs($pool2); -is_deeply( - { - pool_name => $pool_name - }, - $pool2, - 'addProblemPool: add a new problem pool to a course' -); +is($pool2, { pool_name => $pool_name }, 'addProblemPool: add a new problem pool to a course'); # Try to add a pool that already exists. -throws_ok { - $problem_pool_rs->addProblemPool( - params => { - course_name => 'Arithmetic', - pool_name => 'adding fractions' - } - ); -} -'DB::Exception::PoolAlreadyInCourse', 'addProblemPool: pool already exists'; +is( + dies { + $problem_pool_rs->addProblemPool( + params => { course_name => 'Arithmetic', pool_name => 'adding fractions' }); + }, + check_isa('DB::Exception::PoolAlreadyInCourse'), + 'addProblemPool: pool already exists' +); # Try to add a pool with an invalid field. -throws_ok { - $problem_pool_rs->addProblemPool( - params => { - course_name => 'Arithmetic', - pool_name => 'dividing fractions', - other_field => 'XXX' - } - ); -} -'DBIx::Class::Exception', 'addProblemPool: add a pool with non-valid field'; +is( + dies { + $problem_pool_rs->addProblemPool( + params => { course_name => 'Arithmetic', pool_name => 'dividing fractions', other_field => 'XXX' }); + }, + check_isa('DBIx::Class::Exception'), + 'addProblemPool: add a pool with non-valid field' +); # Update an existing problem pool. my $updated_pool = { pool_name => 'subtracting fractions with like denominators', }; @@ -147,27 +144,33 @@ $updated_pool_from_db->{course_name} = 'Arithmetic'; removeIDs($updated_pool_from_db); $updated_pool->{course_name} = 'Arithmetic'; -is_deeply($updated_pool, $updated_pool_from_db, 'updateProblemPool: update the name of a problem pool'); +is($updated_pool_from_db, $updated_pool, 'updateProblemPool: update the name of a problem pool'); # TODO: Try to update a pool that doesn't exist. # Try to get a problem pool from a course that doesn't exist. -throws_ok { - $problem_pool_rs->updateProblemPool( - info => { course_name => 'non_existent_course', pool_name => 'XXXX' }, - parms => $updated_pool - ); -} -'DB::Exception::CourseNotFound', 'udpateProblemPool: update a problem pool from a non-existent course'; +is( + dies { + $problem_pool_rs->updateProblemPool( + info => { course_name => 'non_existent_course', pool_name => 'XXXX' }, + parms => $updated_pool + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'udpateProblemPool: update a problem pool from a non-existent course' +); # Try to get a non-existent problem pool from a course. -throws_ok { - $problem_pool_rs->updateProblemPool( - info => { course_name => 'Arithmetic', pool_name => 'non_existent_pool' }, - params => $updated_pool - ); -} -'DB::Exception::PoolNotInCourse', 'updateProblemPool: update a problem pool from a non-existent course'; +is( + dies { + $problem_pool_rs->updateProblemPool( + info => { course_name => 'Arithmetic', pool_name => 'non_existent_pool' }, + params => $updated_pool + ); + }, + check_isa('DB::Exception::PoolNotInCourse'), + 'updateProblemPool: update a problem pool from a non-existent course' +); # Get all PoolProblems from within a pool my @pool_problems = $problem_pool_rs->getPoolProblems( @@ -211,28 +214,24 @@ is( ); # Check that adding a problem to a non-existence course fails. -throws_ok { - $problem_pool_rs->addProblemToPool( - params => { - course_name => 'non_existing_course', - pool_name => 'adding fractions', - %$prob_to_add - } - ); -} -'DB::Exception::CourseNotFound', 'addProblemToPool: try to add to a nonexisting course'; +is( + dies { + $problem_pool_rs->addProblemToPool( + params => { course_name => 'non_existing_course', pool_name => 'adding fractions', %$prob_to_add }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'addProblemToPool: try to add to a nonexisting course' +); # Check that adding a problem to a non-existence pool fails. -throws_ok { - $problem_pool_rs->addProblemToPool( - params => { - course_name => $updated_pool->{course_name}, - pool_name => 'non_existent_pool_name', - %$prob_to_add - } - ); -} -'DB::Exception::PoolNotInCourse', 'addProblemToPool: try to add to a nonexisting pool'; +is( + dies { + $problem_pool_rs->addProblemToPool(params => + { course_name => $updated_pool->{course_name}, pool_name => 'non_existent_pool_name', %$prob_to_add }); + }, + check_isa('DB::Exception::PoolNotInCourse'), + 'addProblemToPool: try to add to a nonexisting pool' +); # Update a pool problem. my $course_pool_problem_info = {%$updated_pool_from_db}; @@ -252,53 +251,58 @@ is( ); # Check that updating a problem to a non-existence course fails. -throws_ok { - $problem_pool_rs->updatePoolProblem( - info => { - course_name => 'non_existing_course', - pool_name => 'adding fractions', - pool_problem_id => $added_problem->{pool_problem_id} - }, - params => { - params => { - library_id => $updated_library_id - } - } - ); -} -'DB::Exception::CourseNotFound', 'updatePoolProblem: try to update a nonexisting course'; +is( + dies { + $problem_pool_rs->updatePoolProblem( + info => { + course_name => 'non_existing_course', + pool_name => 'adding fractions', + pool_problem_id => $added_problem->{pool_problem_id} + }, + params => { params => { library_id => $updated_library_id } } + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'updatePoolProblem: try to update a nonexisting course' +); # Check that updating a problem to a non-existence course fails. -throws_ok { - $problem_pool_rs->updatePoolProblem( - info => { - course_name => $updated_pool->{course_name}, - pool_name => 'non_existent_pool_name', - pool_problem_id => $added_problem->{pool_problem_id} - }, - params => { library_id => $updated_library_id } - ); -} -'DB::Exception::PoolNotInCourse', 'updatePoolProblem: try to update a nonexisting pool'; +is( + dies { + $problem_pool_rs->updatePoolProblem( + info => { + course_name => $updated_pool->{course_name}, + pool_name => 'non_existent_pool_name', + pool_problem_id => $added_problem->{pool_problem_id} + }, + params => { library_id => $updated_library_id } + ); + }, + check_isa('DB::Exception::PoolNotInCourse'), + 'updatePoolProblem: try to update a nonexisting pool' +); # Check that updating a problem to a non-existing problem fails. -throws_ok { - $problem_pool_rs->updatePoolProblem( - info => { - course_name => $updated_pool->{course_name}, - pool_name => $updated_pool->{pool_name}, - pool_problem_id => -999 - }, - params => { library_id => $updated_library_id } - ); -} -'DB::Exception::PoolProblemNotInPool', 'updatePoolProblem: try to update a nonexisting problem'; +is( + dies { + $problem_pool_rs->updatePoolProblem( + info => { + course_name => $updated_pool->{course_name}, + pool_name => $updated_pool->{pool_name}, + pool_problem_id => -999 + }, + params => { library_id => $updated_library_id } + ); + }, + check_isa('DB::Exception::PoolProblemNotInPool'), + 'updatePoolProblem: try to update a nonexisting problem' +); # Delete a problem pool my $pool_to_delete = $problem_pool_rs->deleteProblemPool(info => $updated_pool); removeIDs($pool_to_delete); $pool_to_delete->{course_name} = 'Arithmetic'; -is_deeply($updated_pool, $pool_to_delete, 'deleteProblemPool: delete an existing problem pool'); +is($pool_to_delete, $updated_pool, 'deleteProblemPool: delete an existing problem pool'); # Ensure that the problem_pool table is restored. @problem_pools_from_db = $problem_pool_rs->getAllProblemPools(); @@ -307,6 +311,6 @@ for my $pool (@problem_pools_from_db) { removeIDs($pool); } -is_deeply(\@problem_pools_from_file, \@problem_pools_from_db, 'check: Ensure that the problem_pool table is restored.'); +is(\@problem_pools_from_db, \@problem_pools_from_file, 'check: Ensure that the problem_pool table is restored.'); done_testing; diff --git a/t/db/009_problems.t b/t/db/009_problems.t index d001c650..86c214b6 100644 --- a/t/db/009_problems.t +++ b/t/db/009_problems.t @@ -14,8 +14,7 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; +use Test2::V0; use Clone qw/clone/; use YAML::XS qw/LoadFile/; @@ -57,7 +56,7 @@ for my $problem (@problems_from_db) { removeIDs($problem); } -is_deeply(\@all_problems, \@problems_from_db, 'getGlobalProblems: get all problems'); +is(\@problems_from_db, \@all_problems, 'getGlobalProblems: get all problems'); # Get all problems from one course. my @precalc_problems_from_db = $problem_rs->getProblems(info => { course_name => 'Precalculus' }); @@ -67,31 +66,25 @@ for my $problem (@precalc_problems_from_db) { # Try to get all problems from a non existent course -throws_ok { - $problem_rs->getProblems( - info => { - course_name => 'non existent course' - } - ); -} -'DB::Exception::CourseNotFound', 'getProblems: non-existent course'; +is( + dies { $problem_rs->getProblems(info => { course_name => 'non existent course' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getProblems: non-existent course' +); # Try to get all problems from a non-existent set_id -throws_ok { - $problem_rs->getProblems( - info => { - course_id => 999999 - } - ); -} -'DB::Exception::CourseNotFound', 'getProblems: non-existent course_id'; +is( + dies { $problem_rs->getProblems(info => { course_id => 999999 }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getProblems: non-existent course_id' +); for my $problem (@precalc_problems) { delete $problem->{set_name}; } -is_deeply(\@precalc_problems, \@precalc_problems_from_db, 'getSetProblems: get all problems from one course'); +is(\@precalc_problems_from_db, \@precalc_problems, 'getSetProblems: get all problems from one course'); # Get all problems in one course from one set. my @set_problems1 = $problem_rs->getSetProblems(info => { course_name => 'Precalculus', set_name => 'HW #1' }); @@ -100,19 +93,21 @@ for my $problem (@set_problems1) { removeIDs($problem); } -is_deeply(\@precalc_problems1, \@set_problems1, 'getSetProblems: get all problems from one set'); +is(\@set_problems1, \@precalc_problems1, 'getSetProblems: get all problems from one set'); # Try to get problems from a non-existing course. -throws_ok { - $problem_rs->getSetProblems(info => { course_name => 'non_existing_course', set_name => 'HW #1' }); -} -'DB::Exception::CourseNotFound', 'getSetProblems: get problems from non-existing course'; +is( + dies { $problem_rs->getSetProblems(info => { course_name => 'non_existing_course', set_name => 'HW #1' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getSetProblems: get problems from non-existing course' +); # Try to get problems from a non-existing set. -throws_ok { - $problem_rs->getSetProblems(info => { course_name => 'Precalculus', set_name => 'HW #999' }); -} -'DB::Exception::SetNotInCourse', 'getSetProblems: get problems from non-existing set'; +is( + dies { $problem_rs->getSetProblems(info => { course_name => 'Precalculus', set_name => 'HW #999' }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'getSetProblems: get problems from non-existing set' +); # Get a single problem from a course. my $set_problem = $problem_rs->getSetProblem( @@ -128,7 +123,7 @@ my $expected_problem = { %{ $problems_from_csv[0] } }; # Copy the first probl delete $expected_problem->{set_name}; delete $expected_problem->{course_name}; -is_deeply($expected_problem, $set_problem, 'getSetProblem: get a single problem from a set in a given course'); +is($set_problem, $expected_problem, 'getSetProblem: get a single problem from a set in a given course'); # Add a problem to an existing set. my $new_problem = { @@ -150,7 +145,7 @@ my $prob1 = $problem_rs->addSetProblem( my $prob_id = $prob1->{set_problem_id}; removeIDs($prob1); -is_deeply($new_problem, $prob1, 'addSetProblem: add a valid problem to a set'); +is($prob1, $new_problem, 'addSetProblem: add a valid problem to a set'); # Add a problem and make sure the problem number is working. # Determine the largest current problem number. @@ -183,63 +178,45 @@ my $prob2 = { 'problem_number' => $max + 1, }; -is_deeply($prob2, $prob2_from_db, 'addSetProblem: add a set problem and ensure the problem number is correct.'); +is($prob2_from_db, $prob2, 'addSetProblem: add a set problem and ensure the problem number is correct.'); # Try to add a problem to a non-existent course -throws_ok { - $problem_rs->addSetProblem( - params => { - course_name => 'Non existent course', - set_name => 'HW #1' - } - ); -} -'DB::Exception::CourseNotFound', 'addSetProblem: try to add a problem to a non-existent course'; +is( + dies { $problem_rs->addSetProblem(params => { course_name => 'Non existent course', set_name => 'HW #1' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'addSetProblem: try to add a problem to a non-existent course' +); # Try to add a problem to a non-existent course_id -throws_ok { - $problem_rs->addSetProblem( - params => { - course_id => 999999, - set_name => 'HW #1' - } - ); -} -'DB::Exception::CourseNotFound', 'addSetProblem: try to add a problem to a non-existent course_id'; +is( + dies { $problem_rs->addSetProblem(params => { course_id => 999999, set_name => 'HW #1' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'addSetProblem: try to add a problem to a non-existent course_id' +); # Try to add a problem to a non-existent set -throws_ok { - $problem_rs->addSetProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #99999' - } - ); -} -'DB::Exception::SetNotInCourse', 'addSetProblem: try to add a problem to a non-existent set'; +is( + dies { $problem_rs->addSetProblem(params => { course_name => 'Precalculus', set_name => 'HW #99999' }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'addSetProblem: try to add a problem to a non-existent set' +); # Try to add a problem to a non-existent set -throws_ok { - $problem_rs->addSetProblem( - params => { - course_name => 'Precalculus', - set_id => 999999 - } - ); -} -'DB::Exception::SetNotInCourse', 'addSetProblem: try to add a problem to a non-existent set_id'; +is( + dies { $problem_rs->addSetProblem(params => { course_name => 'Precalculus', set_id => 999999 }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'addSetProblem: try to add a problem to a non-existent set_id' +); # Try to add a problem without information about the file_path, library_id or problem_pool_id -throws_ok { - $problem_rs->addSetProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #1', - problem_params => {} - } - ); -} -'DB::Exception::FieldsNeeded', "addSetProblem: try to add a problem without information about the file_path, etc."; +is( + dies { + $problem_rs->addSetProblem( + params => { course_name => 'Precalculus', set_name => 'HW #1', problem_params => {} }); + }, + check_isa('DB::Exception::FieldsNeeded'), + "addSetProblem: try to add a problem without information about the file_path, etc." +); # Note: we may want to not have the following in the future, but currently its okay. # Try to add a problem with both information about the file_path, and library_id . @@ -263,7 +240,7 @@ delete $set_prob_params2->{set_name}; removeIDs($set_problem2); delete $set_problem2->{problem_number}; -is_deeply($set_problem2, $set_prob_params2, 'addSetProblem: adding a problem with both file_path and library_id'); +is($set_problem2, $set_prob_params2, 'addSetProblem: adding a problem with both file_path and library_id'); # Update a problem my $updated_params = { @@ -284,82 +261,62 @@ my $updated_problem = $problem_rs->updateSetProblem( ); removeIDs($updated_problem); -is_deeply($all_params, $updated_problem, 'updateProblem: update a problem'); +is($updated_problem, $all_params, 'updateProblem: update a problem'); # Try to update a problem in a non-existent course -throws_ok { - $problem_rs->updateSetProblem( - info => { - course_name => 'Non existent course', - set_name => 'HW #1' - } - ); -} -'DB::Exception::CourseNotFound', 'updateSetProblem: try to update a problem to a non-existent course'; +is( + dies { $problem_rs->updateSetProblem(info => { course_name => 'Non existent course', set_name => 'HW #1' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'updateSetProblem: try to update a problem to a non-existent course' +); # Try to update a problem to with a non-existent course_id -throws_ok { - $problem_rs->updateSetProblem( - info => { - course_id => 999999, - set_name => 'HW #1' - } - ); -} -'DB::Exception::CourseNotFound', 'updateSetProblem: try to update a problem to a non-existent course_id'; +is( + dies { $problem_rs->updateSetProblem(info => { course_id => 999999, set_name => 'HW #1' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'updateSetProblem: try to update a problem to a non-existent course_id' +); # Try to update a problem to a non-existent set -throws_ok { - $problem_rs->updateSetProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #99999' - } - ); -} -'DB::Exception::SetNotInCourse', 'updateSetProblem: try to update a problem to a non-existent set'; +is( + dies { $problem_rs->updateSetProblem(info => { course_name => 'Precalculus', set_name => 'HW #99999' }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'updateSetProblem: try to update a problem to a non-existent set' +); # Try to update a problem to a non-existent set_id -throws_ok { - $problem_rs->updateSetProblem( - info => { - course_name => 'Precalculus', - set_id => 999999 - } - ); -} -'DB::Exception::SetNotInCourse', 'updateSetProblem: try to update a problem to a non-existent set_id'; +is( + dies { $problem_rs->updateSetProblem(info => { course_name => 'Precalculus', set_id => 999999 }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'updateSetProblem: try to update a problem to a non-existent set_id' +); # Try to update a problem with a negative problem number. -throws_ok { - $problem_rs->updateSetProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #1', - problem_number => 1, - }, - params => { - problem_number => -9 - } - ); -} -'DB::Exception::InvalidParameter', - 'updateSetProblem: try to update a problem with a non positive integer problem_number'; +is( + dies { + $problem_rs->updateSetProblem( + info => { course_name => 'Precalculus', set_name => 'HW #1', problem_number => 1, }, + params => { problem_number => -9 } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'updateSetProblem: try to update a problem with a non positive integer problem_number' +); # Try to update a problem without information about the file_path, library_id or problem_pool_id -throws_ok { - $problem_rs->updateSetProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #1', - problem_params => { - file_path => 'this_is_a_path', - library_id => 123 +is( + dies { + $problem_rs->updateSetProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #1', + problem_params => { file_path => 'this_is_a_path', library_id => 123 } } - } - ); -} -'DB::Exception::ParametersNeeded', 'updateSetProblem: try to add a problem with too much library info'; + ); + }, + check_isa('DB::Exception::ParametersNeeded'), + 'updateSetProblem: try to add a problem with too much library info' +); # Delete a problem from a set my $deleted_problem = $problem_rs->deleteSetProblem( @@ -371,7 +328,7 @@ my $deleted_problem = $problem_rs->deleteSetProblem( ); removeIDs($deleted_problem); -is_deeply($updated_problem, $deleted_problem, 'deleteSetProblem: delete one problem in an existing set.'); +is($deleted_problem, $updated_problem, 'deleteSetProblem: delete one problem in an existing set.'); my $deleted_problem2 = $problem_rs->deleteSetProblem( info => { @@ -380,7 +337,7 @@ my $deleted_problem2 = $problem_rs->deleteSetProblem( problem_number => $set_problem_to_delete->{problem_number}, } ); -is_deeply($deleted_problem2, $set_problem_to_delete, 'deleteSetProblem: delete another problem.'); +is($deleted_problem2, $set_problem_to_delete, 'deleteSetProblem: delete another problem.'); my $deleted_problem3 = $problem_rs->deleteSetProblem( info => { @@ -390,7 +347,7 @@ my $deleted_problem3 = $problem_rs->deleteSetProblem( } ); removeIDs($deleted_problem3); -is_deeply($deleted_problem3, $prob2, 'deleteSetProblem: delete another problem.'); +is($deleted_problem3, $prob2, 'deleteSetProblem: delete another problem.'); # Make sure the set_problem table is returned to its orginal state. @problems_from_db = $problem_rs->getGlobalProblems; @@ -398,7 +355,7 @@ for my $problem (@problems_from_db) { removeIDs($problem); } -is_deeply(\@all_problems, \@problems_from_db, 'The set_problems table is returned to its original state.'); +is(\@problems_from_db, \@all_problems, 'The set_problems table is returned to its original state.'); # Ensure that the set_problems table is restored. @problems_from_db = $problem_rs->getGlobalProblems; @@ -406,6 +363,6 @@ for my $problem (@problems_from_db) { removeIDs($problem); } -is_deeply(\@all_problems, \@problems_from_db, 'check: Ensure that the set_problems table is restored.'); +is(\@problems_from_db, \@all_problems, 'check: Ensure that the set_problems table is restored.'); done_testing; diff --git a/t/db/010_user_problems.t b/t/db/010_user_problems.t index 83264eb5..4d58fc70 100644 --- a/t/db/010_user_problems.t +++ b/t/db/010_user_problems.t @@ -1,7 +1,7 @@ #!/usr/bin/env perl -# + # This tests the basic database CRUD functions of user problems. -# + use warnings; use strict; @@ -14,16 +14,12 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; -use Try::Tiny; -use Carp; +use Test2::V0; use Clone qw/clone/; use YAML::XS qw/LoadFile/; use DB::Schema; -use TestUtils qw/loadCSV removeIDs loadSchema/; -use DB::Utils qw/updateAllFields/; +use TestUtils qw/loadCSV removeIDs/; # Set up the database. my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; @@ -104,8 +100,7 @@ for my $user_problem (@all_user_problems_from_db) { # For comparision, from the database needs to be a number. $_->{status} = 0 + $_->{status} for (@all_user_problems_from_db); -is_deeply(\@user_problems_from_csv, \@all_user_problems_from_db, - 'getAllUserProblems: fetch all user problems from the DB.'); +is(\@all_user_problems_from_db, \@user_problems_from_csv, 'getAllUserProblems: fetch all user problems from the DB.'); # Get merged user problems. @@ -124,7 +119,7 @@ $_->{status} = 0 + $_->{status} for (@merged_problems_from_db); @merged_problems_from_db = sort user_prob_sort_fxn @merged_problems_from_db; -is_deeply(\@merged_problems_from_csv, \@merged_problems_from_db, 'getAllUserProblems: fetch all merged problems'); +is(\@merged_problems_from_db, \@merged_problems_from_csv, 'getAllUserProblems: fetch all merged problems'); # Get user problems from one course. @@ -139,11 +134,8 @@ for my $user_problem (@precalc_user_problems_from_db) { @precalc_user_problems_from_db = sort user_prob_sort_fxn @precalc_user_problems_from_db; -is_deeply( - \@precalc_user_problems, - \@precalc_user_problems_from_db, - 'getUserProblems: get user problems from a single course.' -); +is(\@precalc_user_problems_from_db, \@precalc_user_problems, + 'getUserProblems: get user problems from a single course.'); # Get merged problems from one course. @@ -161,11 +153,8 @@ for my $merged_problem (@precalc_merged_problems_from_db) { @precalc_merged_problems = sort user_prob_sort_fxn @precalc_merged_problems; @precalc_merged_problems_from_db = sort user_prob_sort_fxn @precalc_merged_problems_from_db; -is_deeply( - \@precalc_merged_problems, - \@precalc_merged_problems_from_db, - 'getUserProblems: get merged problems from a single course.' -); +is(\@precalc_merged_problems_from_db, + \@precalc_merged_problems, 'getUserProblems: get merged problems from a single course.'); # Get a single user problem. @@ -192,63 +181,71 @@ my $user_problem_from_csv = clone( )[0] ); -is_deeply($user_problem_from_csv, $user_problem, 'getUserProblem: get a single user problem'); +is($user_problem, $user_problem_from_csv, 'getUserProblem: get a single user problem'); # Get a user problem from a course that doesn't exist. - -throws_ok { - $user_problem_rs->getUserProblem( - info => { - course_name => 'course doesn\'t exist', - username => 'homer', - set_name => 'HW #1', - problem_number => 1 - } - ); -} -'DB::Exception::CourseNotFound', 'getUserProblem: attempt to get a user problem from a nonexistent course'; +is( + dies { + $user_problem_rs->getUserProblem( + info => { + course_name => 'course doesn\'t exist', + username => 'homer', + set_name => 'HW #1', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getUserProblem: attempt to get a user problem from a nonexistent course' +); # Get a user problem from a user that doesn't exist. - -throws_ok { - $user_problem_rs->getUserProblem( - info => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1', - problem_number => 1 - } - ); -} -'DB::Exception::UserNotFound', 'getUserProblem: attempt to get a user problem from a nonexistent user'; +is( + dies { + $user_problem_rs->getUserProblem( + info => { + course_name => 'Precalculus', + username => 'non_existent_user', + set_name => 'HW #1', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::UserNotFound'), + 'getUserProblem: attempt to get a user problem from a nonexistent user' +); # Get a user problem from a set that doesn't exist. - -throws_ok { - $user_problem_rs->getUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #99', - problem_number => 1 - } - ); -} -'DB::Exception::SetNotInCourse', 'getUserProblem: attempt to get a user problem from a nonexistent set'; +is( + dies { + $user_problem_rs->getUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #99', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'getUserProblem: attempt to get a user problem from a nonexistent set' +); # Get a user problem from a problem that doesn't exist. - -throws_ok { - $user_problem_rs->getUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 99 - } - ); -} -'DB::Exception::SetProblemNotFound', 'getUserProblem: attempt to get a user problem from a nonexistent problem'; +is( + dies { + $user_problem_rs->getUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #1', + problem_number => 99 + } + ); + }, + check_isa('DB::Exception::SetProblemNotFound'), + 'getUserProblem: attempt to get a user problem from a nonexistent problem' +); # Add a UserProblem to the database. @@ -270,7 +267,7 @@ $problem_info1->{status} = 0; $problem_info1->{problem_params} = {}; $problem_info1->{problem_version} = 1; -is_deeply($problem_info1, $user_problem1, 'addUserProblem: add a single user problem'); +is($user_problem1, $problem_info1, 'addUserProblem: add a single user problem'); # Add a user problem and get back a merged problem. @@ -310,173 +307,195 @@ for my $key (qw/username seed status problem_version/) { } $problem2->{problem_params}->{weight} = 2; -is_deeply($problem2, $user_problem2, 'addUserProblem: add a user problem and return a merged problem'); +is($user_problem2, $problem2, 'addUserProblem: add a user problem and return a merged problem'); # Attempt to add a UserProblem to a non-existent course. - -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'course doesn\'t exist', - username => 'homer', - set_name => 'HW #1', - problem_number => 1 - } - ); -} -'DB::Exception::CourseNotFound', 'addUserProblem: attempt to add a user problem to a nonexistent course'; +is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'course doesn\'t exist', + username => 'homer', + set_name => 'HW #1', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'addUserProblem: attempt to add a user problem to a nonexistent course' +); # Attempt to add a UserProblem for a non-existent problem set. - -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #999', - problem_number => 1 - } - ); -} -'DB::Exception::SetNotInCourse', 'addUserProblem: attempt to add a user problem for a non-existent problem set'; +is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #999', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'addUserProblem: attempt to add a user problem for a non-existent problem set' +); # Attempt to add a UserProblem for a non-existent user. - -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1', - problem_number => 1 - } - ); -} -'DB::Exception::UserNotFound', 'addUserProblem: attempt to add a user problem for a non-existent user'; +is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + username => 'non_existent_user', + set_name => 'HW #1', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::UserNotFound'), + 'addUserProblem: attempt to add a user problem for a non-existent user' +); # Attempt to add a UserProblem for a non-existent problem. - -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 999 - } - ); -} -'DB::Exception::SetProblemNotFound', 'addUserProblem: attempt to add a user problem for a non-existent problem'; +is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #1', + problem_number => 999 + } + ); + }, + check_isa('DB::Exception::SetProblemNotFound'), + 'addUserProblem: attempt to add a user problem for a non-existent problem' +); # Attempt to add a UserProblem that already exists - -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 1 - } - ); -} -'DB::Exception::UserProblemExists', 'addUserProblem: attempt to add a user problem that already exists'; +is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #1', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::UserProblemExists'), + 'addUserProblem: attempt to add a user problem that already exists' +); # Try to add a UserProblem with a bad seed :) . - -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - seed => -1234 - } - ); -} -'DB::Exception::InvalidParameter', 'addUserProblem: attempt to add a user problem with a bad seed'; +is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + seed => -1234 + } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addUserProblem: attempt to add a user problem with a bad seed' +); # Attempt to add a new User Problem with a non existent field - -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - non_existent_field => 1 - } - ); -} -'DBIx::Class::Exception', 'addUserProblem: attempt to add a user problem with a non existent field'; +is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + non_existent_field => 1 + } + ); + }, + check_isa('DBIx::Class::Exception'), + 'addUserProblem: attempt to add a user problem with a non existent field' +); # Attempt to add a new User Problem with a non existent field - -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - non_existent_field => 1 - } - ); -} -qr/No such column 'non_existent_field'/, 'addUserProblem: attempt to add a user problem with a non existent field'; +like( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + non_existent_field => 1 + } + ); + }, + qr/No such column 'non_existent_field'/, + 'addUserProblem: attempt to add a user problem with a non existent field' +); # Attempt to add a new User Problem with invalid library id - -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - problem_params => { - library_id => -1234 +like( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + problem_params => { + library_id => -1234 + } } - } - ); -} -qr/library_id is not valid/, 'addUserProblem: attempt to add a user problem with with invalid library id'; + ); + }, + qr/library_id is not valid/, + 'addUserProblem: attempt to add a user problem with with invalid library id' +); # Attempt to add a new User Problem with a bad problem weight - -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - problem_params => { - weight => -1 +like( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + problem_params => { + weight => -1 + } } - } - ); -} -qr/weight is not valid/, 'addUserProblem: attempt to add a user problem with a bad problem weight'; + ); + }, + qr/weight is not valid/, + 'addUserProblem: attempt to add a user problem with a bad problem weight' +); # Attempt to add a new User Problem with a invalid problem_pool_id - -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - problem_params => { - problem_pool_id => 'fred' +like( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + problem_params => { + problem_pool_id => 'fred' + } } - } - ); -} -qr/problem_pool_id is not valid/, 'addUserProblem: attempt to add a user problem with a bad problem_pool_id'; + ); + }, + qr/problem_pool_id is not valid/, + 'addUserProblem: attempt to add a user problem with a bad problem_pool_id' +); # update a user problem and return as a user problem @@ -493,7 +512,7 @@ $updated_problem1->{status} += 0; delete $updated_problem1->{problem_version} unless defined $updated_problem1->{problem_version}; $user_problem1->{seed} = 4567; -is_deeply($user_problem1, $updated_problem1, 'updateUserProblem: sucessfully update a field'); +is($updated_problem1, $user_problem1, 'updateUserProblem: sucessfully update a field'); # Update a user problem and return as a merged problem. @@ -508,7 +527,7 @@ removeIDs($updated_problem2); $problem2->{seed} = 4567; $updated_problem2->{status} += 0; -is_deeply($problem2, $updated_problem2, 'updateUserProblem: sucessfully update a field and return as a merged problem'); +is($updated_problem2, $problem2, 'updateUserProblem: sucessfully update a field and return as a merged problem'); # Update a user problem in the problem_params @@ -525,176 +544,205 @@ $updated_problem1a->{status} += 0; delete $updated_problem1a->{problem_version} unless defined $updated_problem1a->{problem_version}; $user_problem1->{problem_params}->{library_id} = 1234; -is_deeply($user_problem1, $updated_problem1a, 'updateUserProblem: sucessfully update the problem_params'); +is($updated_problem1a, $user_problem1, 'updateUserProblem: sucessfully update the problem_params'); # Attempt to update a UserProblem to a non-existent course. -throws_ok { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'course doesn\'t exist', - username => 'homer', - set_name => 'HW #1', - problem_number => 1 - }, - params => {} - ); -} -'DB::Exception::CourseNotFound', 'updateUserProblem: attempt to update a user problem to a nonexistent course'; +is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'course doesn\'t exist', + username => 'homer', + set_name => 'HW #1', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'updateUserProblem: attempt to update a user problem to a nonexistent course' +); # Attempt to update a UserProblem for a non-existent problem set. -throws_ok { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #999', - problem_number => 1 - }, - params => {} - ); -} -'DB::Exception::SetNotInCourse', 'updateUserProblem: attempt to update a user problem for a non-existent problem set'; +is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #999', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'updateUserProblem: attempt to update a user problem for a non-existent problem set' +); # Attempt to add a UserProblem for a non-existent user. -throws_ok { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1', - problem_number => 1 - }, - params => {} - ); -} -'DB::Exception::UserNotFound', 'updateUserProblem: attempt to update a user problem for a non-existent user'; +is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + username => 'non_existent_user', + set_name => 'HW #1', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::UserNotFound'), + 'updateUserProblem: attempt to update a user problem for a non-existent user' +); # Attempt to update a UserProblem for a non-existent problem. -throws_ok { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 999 - }, - params => {} - ); -} -'DB::Exception::SetProblemNotFound', 'updateUserProblem: attempt to update a user problem for a non-existent problem'; +is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #1', + problem_number => 999 + }, + params => {} + ); + }, + check_isa('DB::Exception::SetProblemNotFound'), + 'updateUserProblem: attempt to update a user problem for a non-existent problem' +); # Try to update a UserProblem with a bad seed :) . -throws_ok { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - seed => -1234 - } - ); -} -'DB::Exception::InvalidParameter', 'updateUserProblem: attempt to update a user problem with a bad seed'; +is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { + seed => -1234 + } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'updateUserProblem: attempt to update a user problem with a bad seed' +); # Attempt to update a new User Problem with a non existent field -throws_ok { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - non_existent_field => 1 - } - ); -} -'DBIx::Class::Exception', 'updateUserProblem: attempt to update a user problem with a non existent field'; +is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { + non_existent_field => 1 + } + ); + }, + check_isa('DBIx::Class::Exception'), + 'updateUserProblem: attempt to update a user problem with a non existent field' +); # Attempt to update a new User Problem with a non existent field -throws_ok { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - non_existent_field => 1 - } - ); -} -qr/No such column 'non_existent_field'/, - 'updateUserProblem: attempt to update a user problem with a non existent field'; +like( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { + non_existent_field => 1 + } + ); + }, + qr/No such column 'non_existent_field'/, + 'updateUserProblem: attempt to update a user problem with a non existent field' +); # Attempt to update a new User Problem with invalid library id -throws_ok { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - problem_params => { - library_id => -1234 +like( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { + problem_params => { + library_id => -1234 + } } - } - ); -} -qr/library_id is not valid/, 'updateUserProblem: attempt to update a user problem with with invalid library id'; + ); + }, + qr/library_id is not valid/, + 'updateUserProblem: attempt to update a user problem with with invalid library id' +); # Attempt to update a new User Problem with a bad problem weight -throws_ok { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - problem_params => { - weight => -1 +like( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { + problem_params => { + weight => -1 + } } - } - ); -} -qr/weight is not valid/, 'updateUserProblem: attempt to update a user problem with a bad problem weight'; + ); + }, + qr/weight is not valid/, + 'updateUserProblem: attempt to update a user problem with a bad problem weight' +); # Attempt to add a new User Problem with a invalid problem_pool_id -throws_ok { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - problem_params => { - problem_pool_id => 'fred' +like( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { + problem_params => { + problem_pool_id => 'fred' + } } - } - ); -} -qr/problem_pool_id is not valid/, 'updateUserProblem: attempt to update a user problem with a bad problem_pool_id'; + ); + }, + qr/problem_pool_id is not valid/, + 'updateUserProblem: attempt to update a user problem with a bad problem_pool_id' +); # Get an array of user problems for a single user in a course. @@ -712,8 +760,11 @@ for my $user_problem (@user_problems) { my @course_user_problems_from_csv = grep { $_->{course_name} eq 'Precalculus' && $_->{username} eq 'homer' } @user_problems_from_csv; -is_deeply(\@course_user_problems_from_csv, - \@user_problems, 'getCourseUserProblems: get all user problems for a single user in a course'); +is( + \@user_problems, + \@course_user_problems_from_csv, + 'getCourseUserProblems: get all user problems for a single user in a course' +); # Delete a User Problem @@ -724,7 +775,7 @@ $user_problem_to_delete->{status} += 0; delete $user_problem_to_delete->{problem_version} unless defined $user_problem_to_delete->{problem_version}; -is_deeply($user_problem1, $user_problem_to_delete, 'deleteUserProblem: delete a single user problem'); +is($user_problem_to_delete, $user_problem1, 'deleteUserProblem: delete a single user problem'); # Delete a user problem and return as a merged problem. @@ -733,68 +784,79 @@ removeIDs($user_problem_to_delete2); # the status needs be returned to a numerical value. $user_problem_to_delete2->{status} += 0; -is_deeply($problem2, $user_problem_to_delete2, - 'updateUserProblem: sucessfully update a field and return as a merged problem'); +is($user_problem_to_delete2, $problem2, 'updateUserProblem: sucessfully update a field and return as a merged problem'); # Attempt to delete a UserProblem to a non-existent course. -throws_ok { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'course doesn\'t exist', - username => 'homer', - set_name => 'HW #1', - problem_number => 1 - }, - params => {} - ); -} -'DB::Exception::CourseNotFound', 'deleteUserProblem: attempt to delete a user problem to a nonexistent course'; +is( + dies { + $user_problem_rs->deleteUserProblem( + info => { + course_name => 'course doesn\'t exist', + username => 'homer', + set_name => 'HW #1', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteUserProblem: attempt to delete a user problem to a nonexistent course' +); # Attempt to delete a UserProblem for a non-existent problem set. -throws_ok { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #999', - problem_number => 1 - }, - params => {} - ); -} -'DB::Exception::SetNotInCourse', 'deleteUserProblem: attempt to delete a user problem for a non-existent problem set'; +is( + dies { + $user_problem_rs->deleteUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #999', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'deleteUserProblem: attempt to delete a user problem for a non-existent problem set' +); # Attempt to delete a UserProblem for a non-existent user. -throws_ok { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1', - problem_number => 1 - }, - params => {} - ); -} -'DB::Exception::UserNotFound', 'deleteUserProblem: attempt to delete a user problem for a non-existent user'; +is( + dies { + $user_problem_rs->deleteUserProblem( + info => { + course_name => 'Precalculus', + username => 'non_existent_user', + set_name => 'HW #1', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::UserNotFound'), + 'deleteUserProblem: attempt to delete a user problem for a non-existent user' +); # Attempt to delete a UserProblem for a non-existent problem. -throws_ok { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 99 - }, - params => {} - ); -} -'DB::Exception::SetProblemNotFound', 'deleteUserProblem: attempt to delete a user problem for a non-existent problem'; +is( + dies { + $user_problem_rs->deleteUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #1', + problem_number => 99 + }, + params => {} + ); + }, + check_isa('DB::Exception::SetProblemNotFound'), + 'deleteUserProblem: attempt to delete a user problem for a non-existent problem' +); # Ensure that the user_problems table is restored. @all_user_problems_from_db = $user_problem_rs->getAllUserProblems(); @@ -806,7 +868,6 @@ for my $user_problem (@all_user_problems_from_db) { @all_user_problems_from_db = sort user_prob_sort_fxn @all_user_problems_from_db; -is_deeply(\@user_problems_from_csv, \@all_user_problems_from_db, - 'check: Ensure that the set_problems table is restored.'); +is(\@all_user_problems_from_db, \@user_problems_from_csv, 'check: Ensure that the set_problems table is restored.'); done_testing; diff --git a/t/db/011_attempts.t b/t/db/011_attempts.t index 6b599915..05152764 100644 --- a/t/db/011_attempts.t +++ b/t/db/011_attempts.t @@ -1,7 +1,7 @@ #!/usr/bin/env perl -# + # This tests the basic database CRUD functions of attempts. -# + use warnings; use strict; @@ -14,14 +14,11 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; -use Try::Tiny; +use Test2::V0; use YAML::XS qw/LoadFile/; use DB::Schema; -use TestUtils qw/loadCSV removeIDs loadSchema/; -use DB::Utils qw/updateAllFields/; +use TestUtils qw/removeIDs/; # Set up the database. my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; @@ -85,7 +82,7 @@ my $attempt_params1 = { my $attempt1 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params1 }); removeIDs($attempt1); -is_deeply($attempt_params1, $attempt1, 'addAttempt: add an attempt'); +is($attempt1, $attempt_params1, 'addAttempt: add an attempt'); my $attempt_params2 = { scores => [ 0, 1, 1 ], @@ -95,7 +92,7 @@ my $attempt_params2 = { my $attempt2 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params2 }); removeIDs($attempt2); -is_deeply($attempt_params2, $attempt2, 'addAttempt: add another attempt'); +is($attempt2, $attempt_params2, 'addAttempt: add another attempt'); my $attempt_params3 = { scores => [ 0, 0, 0 ], @@ -105,14 +102,17 @@ my $attempt_params3 = { my $attempt3 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params3 }); removeIDs($attempt3); -is_deeply($attempt_params3, $attempt3, 'addAttempt: add yet another attempt'); +is($attempt3, $attempt_params3, 'addAttempt: add yet another attempt'); my @all_attempts = $attempt_rs->getAttempts(info => $user_problem_info); for my $attempt (@all_attempts) { removeIDs($attempt); } -is_deeply([ $attempt_params1, $attempt_params2, $attempt_params3 ], - \@all_attempts, "getAttempts: get attempts for a user problem;"); +is( + \@all_attempts, + [ $attempt_params1, $attempt_params2, $attempt_params3 ], + "getAttempts: get attempts for a user problem;" +); done_testing; diff --git a/t/db/012_set_versions.t b/t/db/012_set_versions.t index 283238f8..e5fac2cb 100644 --- a/t/db/012_set_versions.t +++ b/t/db/012_set_versions.t @@ -14,8 +14,7 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; +use Test2::V0; use YAML::XS qw/LoadFile/; use Clone qw/clone/; @@ -148,7 +147,7 @@ my $user_set1_from_csv = ( # Make a new user set that has a set_version of 1 -is_deeply($user_set1_from_csv, $user_set1, 'getUserSet: get a single user set from a course.'); +is($user_set1, $user_set1_from_csv, 'getUserSet: get a single user set from a course.'); my $user_set1_v1_params = clone $user_set1; $user_set1_v1_params->{set_version} = 1; @@ -157,7 +156,7 @@ my $user_set1_v1 = $user_set_rs->addUserSet(params => { %$user_set_info1, %$user removeIDs($user_set1_v1); cleanUndef($user_set1_v1); -is_deeply($user_set1_v1, $user_set1_v1_params, "addUserSet: add a user set with version =1 "); +is($user_set1_v1, $user_set1_v1_params, "addUserSet: add a user set with version =1 "); # Make a new user set that has a set_version of 2 @@ -168,7 +167,7 @@ my $user_set1_v2 = $user_set_rs->addUserSet(params => { %$user_set_info1, %$user removeIDs($user_set1_v2); cleanUndef($user_set1_v2); -is_deeply($user_set1_v2, $user_set1_v2_params, "addUserSet: add a user set with version = 2."); +is($user_set1_v2, $user_set1_v2_params, "addUserSet: add a user set with version = 2."); my @all_user_set_versions = $user_set_rs->getUserSetVersions(info => $user_set_info1); for my $user_set (@all_user_set_versions) { @@ -176,7 +175,7 @@ for my $user_set (@all_user_set_versions) { cleanUndef($user_set); } -is_deeply( +is( \@all_user_set_versions, [ $user_set1, $user_set1_v1, $user_set1_v2 ], 'getUserSetVersions: get all versions of a user set.' @@ -195,7 +194,7 @@ my $user_set_v1_to_delete = $user_set_rs->deleteUserSet( removeIDs($user_set_v1_to_delete); cleanUndef($user_set_v1_to_delete); -is_deeply($user_set_v1_to_delete, $user_set1_v1, 'deleteUserSet: delete user set with set_version = 1'); +is($user_set_v1_to_delete, $user_set1_v1, 'deleteUserSet: delete user set with set_version = 1'); my $user_set_v2_to_delete = $user_set_rs->deleteUserSet( info => { @@ -208,7 +207,7 @@ my $user_set_v2_to_delete = $user_set_rs->deleteUserSet( removeIDs($user_set_v2_to_delete); cleanUndef($user_set_v2_to_delete); -is_deeply($user_set_v2_to_delete, $user_set1_v2, 'deleteUserSet: delete a versioned user set'); +is($user_set_v2_to_delete, $user_set1_v2, 'deleteUserSet: delete a versioned user set'); # Ensure that the user_sets table is restored. my @all_user_sets_from_db = $user_set_rs->getAllUserSets(merged => 1); @@ -224,6 +223,6 @@ for my $set (@all_user_sets_from_db) { @all_user_sets_from_db = sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @all_user_sets_from_db; -is_deeply(\@all_user_sets_from_db, \@merged_user_sets, 'check: Ensure that the user_sets table is restored.'); +is(\@all_user_sets_from_db, \@merged_user_sets, 'check: Ensure that the user_sets table is restored.'); done_testing; diff --git a/t/db/013_problem_versions.t b/t/db/013_problem_versions.t index 79c06a5c..90101d15 100644 --- a/t/db/013_problem_versions.t +++ b/t/db/013_problem_versions.t @@ -14,8 +14,7 @@ BEGIN { use lib "$main::ww3_dir/lib"; use lib "$main::ww3_dir/t/lib"; -use Test::More; -use Test::Exception; +use Test2::V0; use YAML::XS qw/LoadFile/; use Clone qw/clone/; @@ -102,7 +101,7 @@ my $user_problem1_from_csv = clone( # the status needs be returned to a numerical value. $user_problem1->{status} += 0; -is_deeply($user_problem1_from_csv, $user_problem1, 'getUserProblem: get a single user problem from a course.'); +is($user_problem1, $user_problem1_from_csv, 'getUserProblem: get a single user problem from a course.'); # Make a new user problem that has a problem_version of 2 @@ -112,7 +111,7 @@ $user_problem1_v2_params->{problem_version} = 2; my $user_problem1_v2 = $user_problem_rs->addUserProblem(params => { %$user_problem_info, %$user_problem1_v2_params }); removeIDs($user_problem1_v2); -is_deeply($user_problem1_v2_params, $user_problem1_v2, "addUserProblem: add a user problem with version =2 "); +is($user_problem1_v2, $user_problem1_v2_params, "addUserProblem: add a user problem with version =2 "); # Make a new user set that has a set_version of 3 @@ -122,7 +121,7 @@ $user_problem1_v3_params->{problem_version} = 3; my $user_problem1_v3 = $user_problem_rs->addUserProblem(params => { %$user_problem_info, %$user_problem1_v3_params }); removeIDs($user_problem1_v3); -is_deeply($user_problem1_v3_params, $user_problem1_v3, "addUserProblem: add a user problem with version =3 "); +is($user_problem1_v3, $user_problem1_v3_params, "addUserProblem: add a user problem with version =3 "); my @all_user_problem_versions = $user_problem_rs->getUserProblemVersions(info => $user_problem_info); @@ -131,7 +130,7 @@ for my $user_problem (@all_user_problem_versions) { $user_problem->{status} += 0; } -is_deeply( +is( \@all_user_problem_versions, [ $user_problem1, $user_problem1_v2, $user_problem1_v3 ], 'getUserProblemVersions: get all versions of a user problem' @@ -151,7 +150,7 @@ my $user_problem_v2_to_delete = $user_problem_rs->deleteUserProblem( removeIDs($user_problem_v2_to_delete); $user_problem_v2_to_delete->{status} += 0; -is_deeply($user_problem_v2_to_delete, $user_problem1_v2, 'deleteUserProblem: delete a versioned user problem'); +is($user_problem_v2_to_delete, $user_problem1_v2, 'deleteUserProblem: delete a versioned user problem'); my $user_problem_v3_to_delete = $user_problem_rs->deleteUserProblem( info => { @@ -165,7 +164,7 @@ my $user_problem_v3_to_delete = $user_problem_rs->deleteUserProblem( removeIDs($user_problem_v3_to_delete); $user_problem_v3_to_delete->{status} += 0; -is_deeply($user_problem_v3_to_delete, $user_problem1_v3, 'deleteUserProblem: delete another versioned user problem'); +is($user_problem_v3_to_delete, $user_problem1_v3, 'deleteUserProblem: delete another versioned user problem'); # Ensure that the user_problems table is restored. my @all_user_problems_from_db = $user_problem_rs->getAllUserProblems(); @@ -177,6 +176,6 @@ for my $user_problem (@all_user_problems_from_db) { # For comparision make sure the loaded status are printed to 5 digits. $_->{status} += 0 for (@all_user_problems_from_db); -is_deeply(\@user_problems_from_csv, \@all_user_problems_from_db, 'check: ensure that user_problems table is restored.'); +is(\@all_user_problems_from_db, \@user_problems_from_csv, 'check: ensure that user_problems table is restored.'); done_testing; diff --git a/t/db/README.md b/t/db/README.md index d5b83338..db7a4a41 100644 --- a/t/db/README.md +++ b/t/db/README.md @@ -19,7 +19,7 @@ each test. ## Note -If you get an error, try rerunning steps 2 and 3 above. This rebuids the database +If you get an error, try rerunning steps 2 and 3 above. This rebuilds the database and reruns all of the tests. ## To do diff --git a/t/db/test_course_user.pl b/t/db/test_course_user.pl deleted file mode 100755 index bd9c2fd6..00000000 --- a/t/db/test_course_user.pl +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env perl - -use warnings; -use strict; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::test_dir = abs_path(dirname(__FILE__)); - $main::lib_dir = dirname(dirname($main::test_dir)) . '/lib'; -} - -use lib "$main::lib_dir"; - -use Text::CSV qw/csv/; -use List::Util qw(uniq); -use Test::More; -use Test::Exception; -use Try::Tiny; - -use DB::WithParams; -use DB::WithDates; -use DB::Schema; -use TestUtils qw/loadCSV removeIDs/; - -# load the database -my $db_file = "$main::test_dir/sample_db.sqlite"; -my $schema = DB::Schema->connect("dbi:SQLite:$db_file"); - -my $course_user_rs = $schema->resultset('CourseUser'); - -my $u = $course_user_rs->find({ course_id => 1, user_id => 1 }); - -my $u2 = $u->get_inflated_columns; - diff --git a/t/mojolicious/001_login.t b/t/mojolicious/001_login.t index 8b1c4507..7d3e3d44 100644 --- a/t/mojolicious/001_login.t +++ b/t/mojolicious/001_login.t @@ -2,8 +2,8 @@ use Mojo::Base -strict; -use Test::More; -use Test::Mojo; +use Test2::V0; +use Test2::MojoX; use YAML::XS qw/LoadFile/; use Mojo::JSON qw/true false/; @@ -19,7 +19,7 @@ use lib "$main::ww3_dir/lib"; my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; $config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $t = Test::Mojo->new('WeBWorK3' => LoadFile($config_file)); +my $t = Test2::MojoX->new('WeBWorK3' => LoadFile($config_file)); # Test missing credentials $t->post_ok('/webwork3/api/login')->status_is(500, 'error status')->content_type_is('application/json;charset=UTF-8') diff --git a/t/mojolicious/002_courses.t b/t/mojolicious/002_courses.t index 0626865a..e0f07a74 100644 --- a/t/mojolicious/002_courses.t +++ b/t/mojolicious/002_courses.t @@ -2,8 +2,8 @@ use Mojo::Base -strict; -use Test::More; -use Test::Mojo; +use Test2::V0; +use Test2::MojoX; use Mojo::JSON qw/true false/; BEGIN { @@ -24,7 +24,7 @@ my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; $config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); my $config = clone(LoadFile($config_file)); -my $t = Test::Mojo->new(WeBWorK3 => $config); +my $t = Test2::MojoX->new(WeBWorK3 => $config); # Authenticate with the admin user. $t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200) @@ -52,14 +52,14 @@ my $new_course_id = $t->tx->res->json('/course_id'); $new_course->{course_id} = $new_course_id; # The default for visible is true: $new_course->{visible} = true; -is_deeply($new_course, $t->tx->res->json, "addCourse: courses match"); +is($t->tx->res->json, $new_course, "addCourse: courses match"); # Update the course $new_course->{visible} = true; $t->put_ok("/webwork3/api/courses/$new_course_id" => json => $new_course)->status_is(200) ->json_is('/course_name' => $new_course->{course_name}); -is_deeply($new_course, $t->tx->res->json, 'updateCourse: courses match'); +is($t->tx->res->json, $new_course, 'updateCourse: courses match'); # Testing that booleans returned from the server are JSON booleans. # getting the first course diff --git a/t/mojolicious/003_users.t b/t/mojolicious/003_users.t index b35681ec..949a2638 100644 --- a/t/mojolicious/003_users.t +++ b/t/mojolicious/003_users.t @@ -2,8 +2,8 @@ use Mojo::Base -strict; -use Test::More; -use Test::Mojo; +use Test2::V0; +use Test2::MojoX; use Mojo::JSON qw/true false/; BEGIN { @@ -33,7 +33,7 @@ my $schema = DB::Schema->connect( { quote_names => 1 } ); -my $t = Test::Mojo->new(WeBWorK3 => $config); +my $t = Test2::MojoX->new(WeBWorK3 => $config); # Test all of the user routes with an admin user. @@ -66,12 +66,12 @@ $t->post_ok('/webwork3/api/users' => json => $new_user)->status_is(200) my $new_user_from_db = $t->tx->res->json; $new_user->{user_id} = $new_user_from_db->{user_id}; -is_deeply($new_user, $new_user_from_db, 'addUser: global user added.'); +is($new_user_from_db, $new_user, 'addUser: global user added.'); # Update the user. $new_user->{email} = 'maggie@juno.com'; $t->put_ok("/webwork3/api/users/$new_user->{user_id}" => json => $new_user)->status_is(200); -is_deeply($new_user, $t->tx->res->json, 'updateUser: global user updated'); +is($t->tx->res->json, $new_user, 'updateUser: global user updated'); # Add the user to the course. my $added_user_to_course = { @@ -91,7 +91,7 @@ $t->get_ok('/webwork3/api/courses/1/users/lisa/exists')->status_is(200) $t->get_ok('/webwork3/api/courses/1/users/non_existent_user/exists')->status_is(200) ->content_type_is('application/json;charset=UTF-8'); -is_deeply($t->tx->res->json, {}, 'checkUserExists: check that a non-existent user returns {}'); +is($t->tx->res->json, {}, 'checkUserExists: check that a non-existent user returns {}'); # Testing that booleans returned from the server are JSON booleans. # the first user is the admin diff --git a/t/mojolicious/004_course_users.t b/t/mojolicious/004_course_users.t index 49f44f17..4551269c 100644 --- a/t/mojolicious/004_course_users.t +++ b/t/mojolicious/004_course_users.t @@ -2,8 +2,8 @@ use Mojo::Base -strict; -use Test::More; -use Test::Mojo; +use Test2::V0; +use Test2::MojoX; use Mojo::JSON qw/true false/; BEGIN { @@ -33,7 +33,7 @@ my $schema = DB::Schema->connect( { quote_names => 1 } ); -my $t = Test::Mojo->new(WeBWorK3 => $config); +my $t = Test2::MojoX->new(WeBWorK3 => $config); # Login as an instructor in the Arithmetic course (course_id: 4) $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) diff --git a/t/mojolicious/005_problem_sets.t b/t/mojolicious/005_problem_sets.t index ecb16982..60528298 100644 --- a/t/mojolicious/005_problem_sets.t +++ b/t/mojolicious/005_problem_sets.t @@ -2,12 +2,10 @@ use Mojo::Base -strict; -use Test::More; -use Test::Mojo; +use Test2::V0; +use Test2::MojoX; use Mojo::JSON qw/true false/; -use DateTime::Format::Strptime; - BEGIN { use File::Basename qw/dirname/; use Cwd qw/abs_path/; @@ -29,8 +27,6 @@ my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; $config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); my $config = clone(LoadFile($config_file)); -my $strp = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); - # Connect to the database. my $schema = DB::Schema->connect( $config->{database_dsn}, @@ -38,7 +34,7 @@ my $schema = DB::Schema->connect( $config->{database_password}, { quote_names => 1 } ); -my $t = Test::Mojo->new(WeBWorK3 => $config); +my $t = Test2::MojoX->new(WeBWorK3 => $config); # Login as an instructor. Lisa is an instructor in course_id: 4 (Arithmetic) $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) diff --git a/t/mojolicious/006_quizzes.t b/t/mojolicious/006_quizzes.t index be15d571..0a31ff27 100644 --- a/t/mojolicious/006_quizzes.t +++ b/t/mojolicious/006_quizzes.t @@ -2,12 +2,10 @@ use Mojo::Base -strict; -use Test::More; -use Test::Mojo; +use Test2::V0; +use Test2::MojoX; use Mojo::JSON qw/true false/; -use DateTime::Format::Strptime; - BEGIN { use File::Basename qw/dirname/; use Cwd qw/abs_path/; @@ -29,8 +27,6 @@ my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; $config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); my $config = clone(LoadFile($config_file)); -my $strp = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); - # Connect to the database. my $schema = DB::Schema->connect( $config->{database_dsn}, @@ -39,7 +35,7 @@ my $schema = DB::Schema->connect( { quote_names => 1 } ); -my $t = Test::Mojo->new(WeBWorK3 => $config); +my $t = Test2::MojoX->new(WeBWorK3 => $config); # Login as an user with instructor privileges in a course (Arithmetic; course_id: 4) $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) diff --git a/t/mojolicious/007_review_sets.t b/t/mojolicious/007_review_sets.t index 3f1839f4..86b7dc88 100644 --- a/t/mojolicious/007_review_sets.t +++ b/t/mojolicious/007_review_sets.t @@ -2,8 +2,8 @@ use Mojo::Base -strict; -use Test::More; -use Test::Mojo; +use Test2::V0; +use Test2::MojoX; use Mojo::JSON qw/true false/; BEGIN { @@ -18,10 +18,8 @@ use lib "$main::ww3_dir/t/lib"; use DB::Schema; use Clone qw/clone/; use YAML::XS qw/LoadFile/; -use DateTime::Format::Strptime; use TestUtils qw/loadCSV/; -my $strp = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); # Test the api with common "users" routes. # Load the config file. @@ -37,7 +35,7 @@ my $schema = DB::Schema->connect( { quote_names => 1 } ); -my $t = Test::Mojo->new(WeBWorK3 => $config); +my $t = Test2::MojoX->new(WeBWorK3 => $config); # Login as an user with instructor privileges in a course (Arithmetic; course_id: 4) $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) diff --git a/t/mojolicious/008_problems.t b/t/mojolicious/008_problems.t index 0e130aa5..88620c59 100644 --- a/t/mojolicious/008_problems.t +++ b/t/mojolicious/008_problems.t @@ -2,12 +2,10 @@ use Mojo::Base -strict; -use Test::More; -use Test::Mojo; +use Test2::V0; +use Test2::MojoX; use Mojo::JSON qw/true false/; -use DateTime::Format::Strptime; - BEGIN { use File::Basename qw/dirname/; use Cwd qw/abs_path/; @@ -37,7 +35,7 @@ my $schema = DB::Schema->connect( $config->{database_password}, { quote_names => 1 } ); -my $t = Test::Mojo->new(WeBWorK3 => $config); +my $t = Test2::MojoX->new(WeBWorK3 => $config); # First run tests as logged in as an instructor $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) @@ -80,7 +78,7 @@ $t->get_ok('/webwork3/api/courses/4/problems')->status_is(200)->content_type_is( my $problems_from_db = $t->tx->res->json; for my $problem (@$problems_from_db) { removeIDs($problem); } -is_deeply(\@arith_problems, $problems_from_db, 'getGlobalProblems: get all problems'); +is($problems_from_db, \@arith_problems, 'getGlobalProblems: get all problems'); # Add a new problem to a set. diff --git a/t/mojolicious/009_user_problems.t b/t/mojolicious/009_user_problems.t index 1210baab..6af18b9e 100644 --- a/t/mojolicious/009_user_problems.t +++ b/t/mojolicious/009_user_problems.t @@ -2,12 +2,10 @@ use Mojo::Base -strict; -use Test::More; -use Test::Mojo; +use Test2::V0; +use Test2::MojoX; use Mojo::JSON qw/true false/; -use DateTime::Format::Strptime; - BEGIN { use File::Basename qw/dirname/; use Cwd qw/abs_path/; @@ -37,7 +35,7 @@ my $schema = DB::Schema->connect( $config->{database_password}, { quote_names => 1 } ); -my $t = Test::Mojo->new(WeBWorK3 => $config); +my $t = Test2::MojoX->new(WeBWorK3 => $config); # First run tests as logged in as an instructor $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) @@ -95,7 +93,7 @@ $t->get_ok('/webwork3/api/courses/4/problems')->status_is(200)->content_type_is( my $problems_from_db = $t->tx->res->json; for my $problem (@$problems_from_db) { removeIDs($problem); } -is_deeply(\@arith_problems, $problems_from_db, 'getGlobalProblems: get all problems'); +is($problems_from_db, \@arith_problems, 'getGlobalProblems: get all problems'); # Get all user problems for a single set. @@ -113,7 +111,7 @@ $_->{status} += 0 for (@$user_problems_from_db); my @user_problems_from_db = sort { $a->{username} cmp $b->{username} || $a->{problem_number} <=> $b->{problem_number} } @$user_problems_from_db; -is_deeply(\@user_problems_from_db, \@arith_user_problems, 'getUserProblems: get all problems for a set in a course.'); +is(\@user_problems_from_db, \@arith_user_problems, 'getUserProblems: get all problems for a set in a course.'); # Get all user problems in a course for a single user. Find user 'ralph' @@ -130,8 +128,7 @@ my @ralph_user_problems_from_file = grep { $_->{username} eq 'ralph' } @arith_us # For comparision make sure the loaded status are printed to 5 digits. $_->{status} += 0 for (@$ralph_user_problems); -is_deeply(\@ralph_user_problems_from_file, - $ralph_user_problems, 'getUserProblems: get all problems for a set in a course.'); +is($ralph_user_problems, \@ralph_user_problems_from_file, 'getUserProblems: get all problems for a set in a course.'); # New to make a new problem first diff --git a/t/mojolicious/010_problem_pools.t b/t/mojolicious/010_problem_pools.t index 917780b0..56a8f9a0 100644 --- a/t/mojolicious/010_problem_pools.t +++ b/t/mojolicious/010_problem_pools.t @@ -2,8 +2,8 @@ use Mojo::Base -strict; -use Test::More; -use Test::Mojo; +use Test2::V0; +use Test2::MojoX; BEGIN { use File::Basename qw/dirname/; @@ -18,11 +18,9 @@ use DB::Schema; use Clone qw/clone/; use Mojo::JSON qw/true false/; use YAML::XS qw/LoadFile/; -use DateTime::Format::Strptime; use TestUtils qw/loadCSV removeIDs/; -my $strp = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); # Test the api with common "users" routes. # Load the config file. @@ -38,7 +36,7 @@ my $schema = DB::Schema->connect( { quote_names => 1 } ); -my $t = Test::Mojo->new(WeBWorK3 => $config); +my $t = Test2::MojoX->new(WeBWorK3 => $config); # Login as an user with instructor privileges in a course (Arithmetic; course_id: 4) $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) @@ -79,7 +77,7 @@ for my $pool (@$arith_pools) { } # my @sorted_arith_pools = sort { $a->{pool_name} cmp $b->{pool_name} } @$arith_pools; -is_deeply($arith_pools, \@arith_problem_pools, 'getProblemPools: get problem pools from one course'); +is($arith_pools, \@arith_problem_pools, 'getProblemPools: get problem pools from one course'); $t->get_ok("/webwork3/api/courses/4/pools/$arith_pools_from_db[0]->{problem_pool_id}")->status_is(200) ->json_is('/pool_name' => $arith_problem_pools[0]->{pool_name}); From 43774a130bd7e911c5bf1ede8789a391a3b48f70 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 28 Oct 2022 22:34:04 -0500 Subject: [PATCH 3/3] Massive overhaul of the units tests. The unit tests no longer use the conf/webwork3-test.dist.yml or conf/webwork3-test.yml configuration files. Instead the configuration is hardcoded into the unit test runners defined in t/lib/DBSubtest.pm and bin/dev_scripts/test_pinia_stores.pl. The point is that unit tests should not depend on some modifiable configuration. The conf/webwork3-test.dist.yml file has been deleted. You can copy conf/webwork3.dist.yml to conf/webwork3-dev.yml for development use. For morbo or the `bin/dev_scripts/build_sample_db.pl` script (previously `t/db/build_db.pl`) the file conf/webwork3-dev.yml will be used if it exists. The sample data for the database is now in JSON files instead of CSV files. This allows for structured data with valid types. This means that all of the crap that was done to convert the data from the CSV files into structured data with valid types is not needed. In perl decode_json does what is needed, and in javascript the JSON files can just be imported and typescript correctly infers the types. Note that eventually it would be nice to do what is done in t/db/001_courses.t for more of the tests. That test no longer uses the sample data in the files at all. Most of the tests really don't use much from the sample data. Furthermore, the tests can made much more precise and transparent in this way. The unit tests each deploy a temporary database instance, and load the sample data automatically. In addition, the tests can be run with all three supported databases (sqlite, postgres, and mysql). By default the tests are only run with an in memory sqlite instance. For all of the tests if the environment variable WW3_TEST_ALL_DBS is set then the tests will instead each be run three times. Once with each of the supported databases. For postgres the Test::PostgreSQL package is used to create a temporary postgres instance. For mysql a local modified version of the Test::mysqld package (t/lib/TestMysqld.pm) is used. The upstream Test::mysqld package has issues in that it must be run as root. Note that on the other hand the Test::PostgreSQL package can not be run as root. Note that for these tests to work you must have postgres and mysql (or mariadb) installed. However, they do not need to be running on the system. Since the database instances are temporary and each test uses its own instance, there is no need to be concerned with putting the database back to the way it was. It will be deleted when the test is completed anyway. The bin/dev_scripts/test_pinia_stores bash script has been replaced with the bin/dev_scripts/test_pinia_stores.pl perl script. That script runs a Mojolicious server daemon and creates a webwork3 api app instance with a provided test configuration (no longer in the lib/WeBWorK3.pm file), and then runs the jest pinia store tests. The server port can be configured either with a command line option or via the WW3_TEST_PORT environment variable. The test runs by default only with the in memory sqlite database, but if the -ta (or --test-all-dbs) option is given or the WW3_TEST_ALL_DBS environment variable is set, then the test will run three times, once with each of the three databases. Note that the Github action runs all unit tests with all databases. Previously the pinia stores tests were not run by the action, but now they are (and with all three databases). Converting the CSV files to JSON files and working with that data has illuminated some big problems that need to be fixed. First, the data returned by the webwork3 api methods currently returns data with extremely poor data structure. The data returned by the api methods should not be flattened in the way that it is currently. This is the biggest issue and most pressing. Both the api and the client side stores will need to be fixed. Second, the "get all" methods need to be removed. These are abolutely unnecessary, shouldn't exist, and should not be tested. For example, there is no reason to get all problems in the database. Problems should only be obtained in the context that they have meaning. That is in the context of the set they belong to. It may be that these were added for the purpose of the unit tests to check that things were put back the way that the were in the database. If so, these still shouldn't have been in the code. These methods were all essentially just the DBIx::Class search method which could have been used directly. In addition those checks are now not needed with this change. The getAllProblemSets method was another of these. That is removed. Note that the unit tests now run all all pushes. This is useful before making a pull request to see that the unit tests pass. The code coverage is only uploaded when a pull request is merged into main in any case, so there is no reason not to have these run. --- .eslintignore | 1 + .github/workflows/linter.yml | 32 +- .github/workflows/unit-tests.yml | 20 +- .jscpd.json | 2 +- .perlcriticrc | 4 +- bin/dev_scripts/build_sample_db.pl | 64 ++ bin/dev_scripts/test_pinia_stores | 33 - bin/dev_scripts/test_pinia_stores.pl | 137 +++ bin/setup_db.pl | 26 +- bin/update_perms.pl | 13 +- bin/webwork3 | 7 - conf/webwork3-test.dist.yml | 30 - conf/webwork3.dist.yml | 3 +- lib/DB/Schema/ResultSet/Course.pm | 2 - lib/DB/Schema/ResultSet/ProblemSet.pm | 35 +- lib/DB/Schema/ResultSet/UserSet.pm | 7 +- lib/DB/Utils.pm | 15 +- lib/WeBWorK3.pm | 20 +- lib/WeBWorK3/Controller/ProblemSet.pm | 6 - lib/WeBWorK3/Controller/Settings.pm | 22 +- package.json | 2 +- t/README.md | 44 +- t/db/001_courses.t | 346 +++--- t/db/002_course_settings.t | 510 ++++---- t/db/003_users.t | 533 ++++----- t/db/004_course_users.t | 682 ++++++----- t/db/005_hwsets.t | 799 ++++++------- t/db/006_quizzes.t | 727 ++++++------ t/db/007_user_set.t | 1375 ++++++++++------------ t/db/008_problem_pools.t | 614 +++++----- t/db/009_problems.t | 623 +++++----- t/db/010_user_problems.t | 1534 ++++++++++++------------- t/db/011_attempts.t | 141 +-- t/db/012_set_versions.t | 362 +++--- t/db/013_problem_versions.t | 305 ++--- t/db/README.md | 28 - t/db/build_db.pl | 352 ------ t/db/run_all_tests.pl | 22 - t/db/sample_data/courses.csv | 6 - t/db/sample_data/courses.json | 32 + t/db/sample_data/hw_sets.csv | 13 - t/db/sample_data/hw_sets.json | 161 +++ t/db/sample_data/pool_problems.csv | 19 - t/db/sample_data/pool_problems.json | 55 + t/db/sample_data/problems.csv | 18 - t/db/sample_data/problems.json | 80 ++ t/db/sample_data/quizzes.csv | 7 - t/db/sample_data/quizzes.json | 53 + t/db/sample_data/review_sets.csv | 6 - t/db/sample_data/review_sets.json | 42 + t/db/sample_data/students.csv | 16 - t/db/sample_data/user_problems.csv | 25 - t/db/sample_data/user_problems.json | 96 ++ t/db/sample_data/user_sets.csv | 17 - t/db/sample_data/user_sets.json | 86 ++ t/db/sample_data/users.json | 120 ++ t/lib/BuildDB.pm | 222 ++++ t/lib/DBSubtest.pm | 104 ++ t/lib/TestMysqld.pm | 270 +++++ t/lib/TestUtils.pm | 108 +- t/mojolicious/001_login.t | 111 +- t/mojolicious/002_courses.t | 180 ++- t/mojolicious/003_users.t | 304 +++-- t/mojolicious/004_course_users.t | 288 +++-- t/mojolicious/005_problem_sets.t | 248 ++-- t/mojolicious/006_quizzes.t | 200 ++-- t/mojolicious/007_review_sets.t | 193 ++-- t/mojolicious/008_problems.t | 255 ++-- t/mojolicious/009_user_problems.t | 368 +++--- t/mojolicious/010_problem_pools.t | 327 +++--- tests/stores/courses.spec.ts | 25 +- tests/stores/permissions.spec.ts | 5 +- tests/stores/problem_sets.spec.ts | 40 +- tests/stores/session.spec.ts | 54 +- tests/stores/set_problems.spec.ts | 116 +- tests/stores/user_sets.spec.ts | 98 +- tests/stores/users.spec.ts | 40 +- tests/utils.ts | 112 +- 78 files changed, 6997 insertions(+), 7001 deletions(-) create mode 100755 bin/dev_scripts/build_sample_db.pl delete mode 100755 bin/dev_scripts/test_pinia_stores create mode 100755 bin/dev_scripts/test_pinia_stores.pl delete mode 100644 conf/webwork3-test.dist.yml delete mode 100644 t/db/README.md delete mode 100755 t/db/build_db.pl delete mode 100755 t/db/run_all_tests.pl delete mode 100644 t/db/sample_data/courses.csv create mode 100644 t/db/sample_data/courses.json delete mode 100644 t/db/sample_data/hw_sets.csv create mode 100644 t/db/sample_data/hw_sets.json delete mode 100644 t/db/sample_data/pool_problems.csv create mode 100644 t/db/sample_data/pool_problems.json delete mode 100644 t/db/sample_data/problems.csv create mode 100644 t/db/sample_data/problems.json delete mode 100644 t/db/sample_data/quizzes.csv create mode 100644 t/db/sample_data/quizzes.json delete mode 100644 t/db/sample_data/review_sets.csv create mode 100644 t/db/sample_data/review_sets.json delete mode 100644 t/db/sample_data/students.csv delete mode 100644 t/db/sample_data/user_problems.csv create mode 100644 t/db/sample_data/user_problems.json delete mode 100644 t/db/sample_data/user_sets.csv create mode 100644 t/db/sample_data/user_sets.json create mode 100644 t/db/sample_data/users.json create mode 100644 t/lib/BuildDB.pm create mode 100644 t/lib/DBSubtest.pm create mode 100644 t/lib/TestMysqld.pm diff --git a/.eslintignore b/.eslintignore index a15c8e3c..f0233af8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,3 +4,4 @@ package.json package-lock.json tsconfig.json +t/db/sample_data/*.json diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 559abec1..22864619 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,60 +1,38 @@ --- -########################### -## Linter GitHub Actions ## -########################### name: Lint Code Base defaults: run: shell: bash -# Documentation: -# https://help.github.com/en/articles/workflow-syntax-for-github-actions - -############################# -# Start the job on all push # -############################# on: push: branches-ignore: [main] - # Remove the line above to run when pushing to main pull_request: branches: [main] -############### -# Set the Job # -############### jobs: lint: - # Name the Job name: Lint Code Base - # Set the agent to run on runs-on: ubuntu-latest - ################## - # Load all steps # - ################## steps: - ########################## - # Checkout the code base # - ########################## + # Checkout the code base - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # Full git history is needed to get a proper list of changed files within `super-linter` fetch-depth: 0 # Install node (for npm) and use it to install eslint and stylelint dependencies. - name: Use Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: '16' - name: Install Dependencies run: npm ci - ################################ - # Run Linter against code base # - ################################ + # Run Linter against code base - name: Super-Linter uses: github/super-linter@v4.9.5 @@ -77,7 +55,7 @@ jobs: container: image: perl:5.32 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: perl -V run: perl -V - name: Install dependencies diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 42ca9be3..a8049e25 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -3,7 +3,6 @@ name: Unit Tests and Coverage on: push: - branches: [main] pull_request: branches: [main] @@ -39,6 +38,7 @@ jobs: libdata-dump-perl \ libdatetime-format-strptime-perl \ libdbd-sqlite3-perl \ + libdbd-mysql-perl \ libdbix-class-inflatecolumn-serializer-perl \ libdbix-class-perl \ libdbix-dbschema-perl \ @@ -61,9 +61,13 @@ jobs: libterm-table-perl \ libtest-harness-perl \ libtest2-suite-perl \ + libtest-postgresql-perl \ libtext-csv-perl \ libtry-tiny-perl \ - libyaml-libyaml-perl + libyaml-libyaml-perl \ + mariadb-client \ + mariadb-server \ + postgresql cpanm --sudo --notest \ DBIx::Class::DynamicSubclass \ Mojolicious::Plugin::DBIC \ @@ -74,9 +78,8 @@ jobs: - name: Run perl unit tests env: HARNESS_PERL_SWITCHES: -MDevel::Cover - run: | - perl t/db/build_db.pl - prove -r t + WW3_TEST_ALL_DBS: 1 + run: prove -r t - name: Push coverage analysis if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} @@ -93,5 +96,10 @@ jobs: - name: Install Dependencies run: npm ci - - name: Run typescript (client-side) tests + - name: Run jest client-side tests run: npm run test + + - name: Run jest pinia stores tests. + env: + WW3_TEST_ALL_DBS: 1 + run: bin/dev_scripts/test_pinia_stores.pl diff --git a/.jscpd.json b/.jscpd.json index 8aead4ac..26404f6e 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -1,4 +1,4 @@ { "threshold": 5, - "ignore": ["node_modules", "dist", "**/*.pm", "**/*.ts"] + "ignore": ["node_modules", "dist", "**/*.pm", "**/*.ts", "**/t/db/sample_data/*.json"] } diff --git a/.perlcriticrc b/.perlcriticrc index b3d5fb42..784e32ac 100644 --- a/.perlcriticrc +++ b/.perlcriticrc @@ -9,6 +9,6 @@ allow_with_category_restriction = 1 # Allow $a and $b in sort functions. If sort functions are added to the code not in a "sort" call, they must be added # to this list. Annoyingly both of these policies that do the same thing have to each get the list. [Community::DollarAB] -extra_pair_functions = user_prob_sort_fxn +extra_pair_functions = user_prob_sort_fcn [Freenode::DollarAB] -extra_pair_functions = user_prob_sort_fxn +extra_pair_functions = user_prob_sort_fcn diff --git a/bin/dev_scripts/build_sample_db.pl b/bin/dev_scripts/build_sample_db.pl new file mode 100755 index 00000000..5f07521a --- /dev/null +++ b/bin/dev_scripts/build_sample_db.pl @@ -0,0 +1,64 @@ +#!/usr/bin/env perl + +# This file fills a database with sample data from JSON files. + +use warnings; +use strict; +use feature 'say'; + +use Mojo::File qw/curfile/; +use YAML::XS qw/LoadFile/; + +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->dirname->sibling('t/lib')->to_string; + +use DB::Schema; +use BuildDB qw/loadPermissions addCourses addUsers addSets addProblems addUserSets addProblemPools addUserProblems/; + +my $ww3_dir = curfile->dirname->dirname->dirname; + +# Load the configuration for the database settings. +my $config_file = $ww3_dir->child('conf', 'webwork3-dev.yml'); +$config_file = $ww3_dir->child('conf/webwork3.yml') unless -e $config_file; +$config_file = $ww3_dir->child('conf/webwork3.dist.yml') unless -e $config_file; +my $config = LoadFile($config_file); + +# Connect to the database. +my $schema = DB::Schema->connect( + $config->{database_dsn}, + $config->{database_user}, + $config->{database_password}, + { quote_names => 1 } +); + +say "restoring the database with dbi: $config->{database_dsn}"; + +# Create the database based on the schema. +$schema->deploy({ add_drop_table => 1 }); + +# The permissions need to be loaded into the database first. +say 'loading permissions'; +loadPermissions($schema, $ww3_dir); + +say 'adding courses'; +addCourses($schema, $ww3_dir); + +say 'adding users'; +addUsers($schema, $ww3_dir); + +say 'adding problem sets'; +addSets($schema, $ww3_dir); + +say 'adding problems'; +addProblems($schema, $ww3_dir); + +say 'adding user sets'; +addUserSets($schema, $ww3_dir); + +say 'adding problem pools'; +addProblemPools($schema, $ww3_dir); + +say 'adding user problems'; +addUserProblems($schema, $ww3_dir); + +1; diff --git a/bin/dev_scripts/test_pinia_stores b/bin/dev_scripts/test_pinia_stores deleted file mode 100755 index 06934c4f..00000000 --- a/bin/dev_scripts/test_pinia_stores +++ /dev/null @@ -1,33 +0,0 @@ -#! /usr/bin/env bash - -# Runs the tests in the tests/store directory by first spawning morbo (needed to test the stores) -# The port can be changed either with the environmental variable WEBWORK3_TEST_PORT -# or the command line option -p (which overrides WEBWORK3_TEST_PORT) - -port=3333 -if [[ -n ${WEBWORK3_TEST_PORT+x} ]]; then - port=$WEBWORK3_TEST_PORT -fi - -while getopts ':p:' OPTION; do - case "$OPTION" in - p) - port="$OPTARG" - echo "The port has been set to $port via a command line argument" - ;; - *) - echo "the option $OPTARG is not defined" - exit 1 - ;; - esac -done - -echo "Running the test scripts using port $port" - -morbo -v -m test -l "http://[::]:$port" bin/webwork3 & -# Grab the process number. -P1=$! - -npx jest --verbose --runInBand --testURL "http://localhost:$port/webwork3/api" 'tests/stores' - -kill $P1 diff --git a/bin/dev_scripts/test_pinia_stores.pl b/bin/dev_scripts/test_pinia_stores.pl new file mode 100755 index 00000000..ba4aad6f --- /dev/null +++ b/bin/dev_scripts/test_pinia_stores.pl @@ -0,0 +1,137 @@ +#!/usr/bin/env perl + +=head1 NAME + +test_pinia_stores.pl - Run the webwork3 client side pinia stores unit tests. + +=head1 SYNOPSIS + +test_pinia_stores.pl [options] + + Options: + -p |--port=port_number Use port_number for the server instance. + (Default: 3333) + + -ta|--test-all-dbs Test with all supported database types, currently + sqlite, postgres, and mysql. If this is not set + the tests will run once with sqlite. + + -h |--help Show full help + +=head1 DESCRIPTION + +This script runs a Mojolicious server daemon that serves the webwork3 server api +app. It then deploys the webwork3 database, and fills it with sample data from +JSON files. Finally, it runs the jest pinia stores tests in a subprocess. + +The port used by the server daemon defaults to port 3333. That can be changed +with the -p option, or by setting the environment variable WW3_TEST_PORT to the +desired port. + +By default the tests are run only once using an in memory sqlite database. +However if the -ta option is given or the environment variable WW3_TEST_ALL_DBS +is set to a truthy value, then the tests are run three times consecutively with +an in memory sqlite database, then with a postgres databaase, and then with a +mysql database. The postgres and mysql database instances are temporary +instances created via the Test::PostgreSQL package and a modified local version +of the Test::mysqld package (TestMysqld located in t/lib). + +=cut + +use Mojo::Base; +use Mojo::File qw(curfile); +use Mojo::IOLoop::Subprocess; +use Mojo::Server::Daemon; +use Test::PostgreSQL; +use Getopt::Long qw(:config bundling_override); +use Pod::Usage; + +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->dirname->sibling('t/lib')->to_string; + +use BuildDB qw/loadPermissions addCourses addUsers addSets addProblems addUserSets addProblemPools addUserProblems/; +use TestMysqld; + +GetOptions('p|port=s' => \my $port, 'ta|test-all-dbs' => \my $test_all_dbs, 'h|help' => \my $show_help) + or pod2usage({ -verbose => 1, -exitval => 1 }); +pod2usage({ -verbose => 2, -exitval => 0, -noperldoc => 1 }) if $show_help; + +$port //= $ENV{WW3_TEST_PORT} // '3333'; +$test_all_dbs //= $ENV{WW3_TEST_ALL_DBS} // 0; + +die qq{The node_modules directory was not detected or is not readable. Have you run "npm install"?\n} + unless -r curfile->dirname->dirname->sibling('node_modules'); + +for my $db_type ($test_all_dbs ? ('sqlite', 'postgres', 'mysql') : 'sqlite') { + print "Testing pinia stores with database $db_type\n"; + + my ($sqld, $dsn); + + # Load the database. + if ($db_type eq 'postgres') { + $sqld = eval { Test::PostgreSQL->new } or die "Unable to initialize psql instance\n"; + $dsn = $sqld->dsn; + } elsif ($db_type eq 'mysql') { + $sqld = eval { TestMysqld->new(my_cnf => { 'skip-networking' => '' }) } + or die $TestMysqld::errstr; + $dsn = $sqld->dsn; + } else { + $dsn = 'dbi:SQLite:dbname=:memory:'; + } + + # Setup the webwork3 api app. + my $app = Mojo::Server->new->build_app( + 'WeBWorK3' => { + config => { + config_override => 1, + secrets => ['1234'], + database_dsn => $dsn, + cookie_secure => 0, + cookie_lifetime => 3600, + $db_type eq 'postgres' ? (database_on_connect_do => 'SET client_min_messages=WARNING;') : () + } + } + ); + + $app->log->path($app->home->child('logs', 'webwork3_test.log')); + + my $schema = $app->schema; + + # Deploy the database. + $schema->deploy({ add_drop_table => 1 }); + + # Load sample data used by the store tests. + loadPermissions($schema, $app->home); + addCourses($schema, $app->home); + addUsers($schema, $app->home); + addSets($schema, $app->home); + addProblems($schema, $app->home); + addUserSets($schema, $app->home); + addProblemPools($schema, $app->home); + addUserProblems($schema, $app->home); + + # Start the server. + my $daemon = Mojo::Server::Daemon->new(app => $app, listen => ["http://[::]:$port"]); + $daemon->start; + + Mojo::IOLoop->subprocess->run( + sub { + # Run the tests. + system( + 'npx', 'jest', '--verbose', '--runInBand', '--testURL', + "http://localhost:$port/webwork3/api", + $app->home->child('tests/stores') + ); + }, + sub { + # This must be done here for postgres or exceptions are thrown after the test finishes in some cases + # because the postgres daemon can stop before the Mojolicious app disconnects the schema from the + # database. It doesn't hurt for the others. + $schema->storage->disconnect; + + Mojo::IOLoop->stop; + } + ); + + Mojo::IOLoop->start unless Mojo::IOLoop->is_running; +} diff --git a/bin/setup_db.pl b/bin/setup_db.pl index 63c26b2b..bbc03a49 100755 --- a/bin/setup_db.pl +++ b/bin/setup_db.pl @@ -75,20 +75,20 @@ BEGIN # List all databases, and if the database does not already exist, then create it. if (!grep { $_->[0] eq $database_name } @{ $dbh->selectall_arrayref('SHOW DATABASES') }) { - say "Creating database '$database_name'."; + say qq{Creating database "$database_name".}; $dbh->do("CREATE DATABASE `$database_name`"); } else { - say "Not Creating database '$database_name'. Database already exists."; + say qq{Not Creating database "$database_name". Database already exists.}; } # List all users, and if the user does not already exist, then create it. if (!grep { $_->[0] eq $config->{database_user} } @{ $dbh->selectall_arrayref('SELECT user FROM mysql.user') }) { - say "Creating user '$config->{database_user}'."; + say qq{Creating user "$config->{database_user}".}; $dbh->do("CREATE USER '$config->{database_user}'\@'localhost' " . "IDENTIFIED BY '$config->{database_password}'"); } else { - say "Not creating user '$config->{database_user}'. User already exists."; + say qq{Not creating user "$config->{database_user}". User already exists.}; } # Grant the necessary permissions to the user. @@ -121,19 +121,19 @@ BEGIN # List all databases, and if the database does not already exist, then create it. if (!grep { $_->[0] eq $database_name } @{ $dbh->selectall_arrayref('SELECT datname FROM pg_database') }) { - say "Creating database '$database_name'."; + say qq{Creating database "$database_name".}; $dbh->do(qq{CREATE DATABASE "$database_name"}); } else { - say "Not Creating database '$database_name'. Database already exists."; + say qq{Not Creating database "$database_name". Database already exists.}; } # List all users, and if the user does not already exist, then create it. if (!grep { $_->[0] eq $config->{database_user} } @{ $dbh->selectall_arrayref('SELECT usename FROM pg_user') }) { - say "Creating user '$config->{database_user}'."; + say qq{Creating user "$config->{database_user}".}; $dbh->do(qq{CREATE USER "$config->{database_user}" WITH PASSWORD '$config->{database_password}'}); } else { - say "Not creating user '$config->{database_user}'. User already exists."; + say qq{Not creating user "$config->{database_user}". User already exists.}; } } catch { say 'ERROR: There was an error communicating with postgres.'; @@ -147,11 +147,17 @@ BEGIN $config->{database_dsn}, $config->{database_user}, $config->{database_password}, - { quote_names => 1 } + { + quote_names => 1, + $database_type eq 'Pg' ? (on_connect_do => 'SET client_min_messages=WARNING;') : () + } ); # Deploy the database as specified by the webwork3 schema. -say "Setting up the database with dsn '$config->{database_dsn}'"; +say qq{Setting up the database with dsn "$config->{database_dsn}".}; $schema->deploy({ add_drop_table => 1 }); +# Add the initial admin user. +$schema->resultset('User')->create({ username => 'admin', is_admin => 1, login_params => { password => 'admin' } }); + 1; diff --git a/bin/update_perms.pl b/bin/update_perms.pl index 2385b535..912e4c09 100755 --- a/bin/update_perms.pl +++ b/bin/update_perms.pl @@ -37,6 +37,7 @@ BEGIN use Getopt::Long qw(:config bundling); use Pod::Usage; use DB::Schema; +use YAML::XS qw/LoadFile/; use DB::Utils qw/updatePermissions/; @@ -47,11 +48,19 @@ BEGIN # Load the configuration to obtain the database settings. my $ww3_conf = "$main::webwork3_dir/conf/webwork3.yml"; $ww3_conf = "$main::webwork3_dir/conf/webwork3.dist.yml" unless -r $ww3_conf; +my $config = LoadFile($ww3_conf); + +# Connect to the database. +my $schema = DB::Schema->connect( + $config->{database_dsn}, + $config->{database_user}, + $config->{database_password}, + { quote_names => 1 } +); my $role_perm_file = "$main::webwork3_dir/conf/permissions.yml"; -# if it doesn't exist, load the default one: $role_perm_file = "$main::webwork3_dir/conf/permissions.dist.yml" unless -r $role_perm_file; -updatePermissions($ww3_conf, $role_perm_file); +updatePermissions($schema, $role_perm_file); 1; diff --git a/bin/webwork3 b/bin/webwork3 index ba741199..d3e97905 100755 --- a/bin/webwork3 +++ b/bin/webwork3 @@ -8,12 +8,5 @@ use Mojo::File qw(curfile); use lib curfile->dirname->sibling('lib')->to_string; use Mojolicious::Commands; -# Check if the config file has been created. -my $webwork_dir = curfile->dirname->dirname(".."); - -warn qq!The file $webwork_dir/conf/webwork3.yml does not exist. - Perhaps you haven't copied webwork3.yml.dist to this file.! - unless -e "$webwork_dir/conf/webwork3.yml"; - # Start command line interface for application. Mojolicious::Commands->start_app('WeBWorK3'); diff --git a/conf/webwork3-test.dist.yml b/conf/webwork3-test.dist.yml deleted file mode 100644 index 0fd0a819..00000000 --- a/conf/webwork3-test.dist.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- -secrets: - - 3cdf63327fcf77deaed1d200df4b9fee66af2326 -webwork3_home: . - -# Database settings - -# These settings are used for running unit_tests. You should choose a database -# that is separate from any either production or other testing database. The -# sqlite one is recommended. If you choose another full-featured DB, select a -# database that is different than others. - -# For the sqlite database -database_dsn: dbi:SQLite:./t/db/sample_db.sqlite -# For mysql or mariadb -# database_dsn: dbi:mysql:dbname=webwork3_test -# For postgres -#database_dsn: dbi:Pg:dbname=webwork3_test;host=localhost - -# Database credentials for mysql, mariadb, or postgres. -# These are ignored if the 'sqlite' database is used. -database_user: webworkWrite -database_password: password - -# Cookie control settings -# cookie_samesite: None -# disable this for testing. -cookie_secure: 0 -cookie_lifetime: 3600 - diff --git a/conf/webwork3.dist.yml b/conf/webwork3.dist.yml index 7cbfe510..01aabf53 100644 --- a/conf/webwork3.dist.yml +++ b/conf/webwork3.dist.yml @@ -1,12 +1,11 @@ --- secrets: - 3cdf63327fcf77deaed1d200df4b9fee66af2326 -webwork3_home: . # Database settings # For the sqlite database -database_dsn: dbi:SQLite:./t/db/sample_db.sqlite +database_dsn: dbi:SQLite:./sample_db.sqlite # For mysql or mariadb #database_dsn: dbi:mysql:dbname=webwork3 # For postgres diff --git a/lib/DB/Schema/ResultSet/Course.pm b/lib/DB/Schema/ResultSet/Course.pm index 08294bc0..d3ef3f6e 100644 --- a/lib/DB/Schema/ResultSet/Course.pm +++ b/lib/DB/Schema/ResultSet/Course.pm @@ -237,8 +237,6 @@ sub getUserCourses ($self, %args) { my $user = $self->result_source->schema->resultset('User') ->getGlobalUser(info => getUserInfo($args{info}), as_result_set => 1); - # my @user_courses = $user->courses->search({}); - my @user_courses = $user->course_users->search({}, { prefetch => [qw/role/] }); return @user_courses if $args{as_result_set}; diff --git a/lib/DB/Schema/ResultSet/ProblemSet.pm b/lib/DB/Schema/ResultSet/ProblemSet.pm index c41e6fcc..766599ed 100644 --- a/lib/DB/Schema/ResultSet/ProblemSet.pm +++ b/lib/DB/Schema/ResultSet/ProblemSet.pm @@ -28,41 +28,8 @@ C. The basics are a CRUD for ProblemSets. Note: a ProblemSet is an abstract class for HWSet, Quiz, ReviewSet, which differ in parameter and dates types. -=head2 getProblemSets - -This gets a list of all ProblemSet (and set-like objects) stored in the database -in the C table. - -=head3 input - -=over -=item - C, a boolean. If true this result an array of C -if false, an array of hashrefs of ProblemSet. - -=back - -=head3 output - -An array of courses as a C object. - =cut -sub getAllProblemSets ($self, %args) { - my @problem_sets = $self->search(); - - return @problem_sets if $args{as_result_set}; - - my @all_sets = (); - for my $set (@problem_sets) { - my $expanded_set = - { $set->get_inflated_columns, $set->courses->get_inflated_columns, set_type => $set->set_type }; - delete $expanded_set->{type}; - push(@all_sets, $expanded_set); - } - - return @all_sets; -} - # The following is CRUD for problem sets in a given course =head2 getProblemSets @@ -333,7 +300,7 @@ sub addProblemSet { $set_params->{type} = $SET_TYPES->{ $set_params->{set_type} || 'HW' }; # Delete a few fields that may be passed in but are not in the database # Note: on client-side set_id=0 means that the set is new, so delete this - # and it will be determined. + # and it will be determined. for my $key (qw/course_id course_name set_type set_id/) { delete $set_params->{$key} if defined $set_params->{$key}; } diff --git a/lib/DB/Schema/ResultSet/UserSet.pm b/lib/DB/Schema/ResultSet/UserSet.pm index 344f0b3b..9da3ecb6 100644 --- a/lib/DB/Schema/ResultSet/UserSet.pm +++ b/lib/DB/Schema/ResultSet/UserSet.pm @@ -61,12 +61,7 @@ That is, a C (HWSet, Quiz, ...) with UserSet overrides. =cut sub getAllUserSets ($self, %args) { - my @user_sets = $self->search( - {}, - { - join => [ { 'problem_set' => 'courses' }, { 'course_users' => 'users' } ] - } - ); + my @user_sets = $self->search({}, { join => [ { 'problem_set' => 'courses' }, { 'course_users' => 'users' } ] }); return @user_sets if $args{as_result_set}; diff --git a/lib/DB/Utils.pm b/lib/DB/Utils.pm index 04e037d9..bf5bb070 100644 --- a/lib/DB/Utils.pm +++ b/lib/DB/Utils.pm @@ -98,24 +98,11 @@ The updatePermissions subroutine loads the roles and permissions from a YAML fil =cut -sub updatePermissions ($ww3_conf, $role_perm_file) { - - my $config = LoadFile($ww3_conf); - - # Connect to the database. - my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } - ); - +sub updatePermissions ($schema, $role_perm_file) { # load any YAML true/false as booleans, not string true/false. local $YAML::XS::Boolean = "JSON::PP"; my $role_perm = LoadFile($role_perm_file); - print "Rebuilding all roles and permissions in database\n"; - # clear out the tables role, db_perm, ui_perm $schema->resultset('Role')->delete_all; $schema->resultset('DBPermission')->delete_all; diff --git a/lib/WeBWorK3.pm b/lib/WeBWorK3.pm index cb8163a0..7a3942b4 100644 --- a/lib/WeBWorK3.pm +++ b/lib/WeBWorK3.pm @@ -16,21 +16,14 @@ sub startup ($app) { $config_file = $app->home->child('conf', 'webwork3.yml'); $config_file = $app->home->child('conf', 'webwork3.dist.yml') unless -e $config_file; - } elsif ($ENV{MOJO_MODE} && $ENV{MOJO_MODE} eq 'test') { - $app->log->path($app->home->child('logs', 'webwork3_test.log')); - $app->log->level('trace'); - - $config_file = $app->home->child('conf', 'webwork3-test.yml'); - $config_file = $app->home->child('conf', 'webwork3-test.dist.yml') unless -e $config_file; - $app->plugin(NotYAMLConfig => { file => $config_file }); } else { $config_file = $app->home->child('conf', 'webwork3-dev.yml'); $config_file = $app->home->child('conf', 'webwork3.yml') unless -e $config_file; $config_file = $app->home->child('conf', 'webwork3.dist.yml') unless -e $config_file; } - # Load configuration from config file - my $config = $app->plugin(NotYAMLConfig => { file => $config_file }); + # Load the configuration from the config file, or for unit tests get the supplied config. + my $config = $config_file ? $app->plugin(NotYAMLConfig => { file => $config_file }) : $app->config; # Configure the application $app->secrets($config->{secrets}); @@ -39,8 +32,13 @@ sub startup ($app) { $app->plugin( DBIC => { schema => DB::Schema->connect( - $config->{database_dsn}, $config->{database_user}, - $config->{database_password}, { quote_names => 1 } + $config->{database_dsn}, + $config->{database_user}, + $config->{database_password}, + { + quote_names => 1, + $config->{database_on_connect_do} ? (on_connect_do => $config->{database_on_connect_do}) : () + } ) } ); diff --git a/lib/WeBWorK3/Controller/ProblemSet.pm b/lib/WeBWorK3/Controller/ProblemSet.pm index 739662c5..e86f893a 100644 --- a/lib/WeBWorK3/Controller/ProblemSet.pm +++ b/lib/WeBWorK3/Controller/ProblemSet.pm @@ -7,12 +7,6 @@ use Mojo::Base 'Mojolicious::Controller', -signatures; use Try::Tiny; use Mojo::JSON qw/true false/; -sub getAllProblemSets ($self) { - my @all_problem_sets = $self->schema->resultset('ProblemSet')->getAllProblemSets; - $self->render(json => \@all_problem_sets); - return; -} - sub getProblemSets ($self) { my @problem_sets = $self->schema->resultset('ProblemSet')->getProblemSets(info => { course_id => int($self->param('course_id')) }); diff --git a/lib/WeBWorK3/Controller/Settings.pm b/lib/WeBWorK3/Controller/Settings.pm index 7e959477..65f50aff 100644 --- a/lib/WeBWorK3/Controller/Settings.pm +++ b/lib/WeBWorK3/Controller/Settings.pm @@ -16,17 +16,17 @@ use YAML::XS qw/LoadFile/; # This reads the default settings from a file. -sub getDefaultCourseSettings ($self) { - my $settings = LoadFile(path($self->config->{webwork3_home}, 'conf', 'course_defaults.yml')); +sub getDefaultCourseSettings ($c) { + my $settings = LoadFile($c->app->home->child('conf', 'course_defaults.yml')); # Check if the file exists. - $self->render(json => $settings); + $c->render(json => $settings); return; } -sub getCourseSettings ($self) { - my $course_settings = $self->schema->resultset('Course')->getCourseSettings( +sub getCourseSettings ($c) { + my $course_settings = $c->schema->resultset('Course')->getCourseSettings( info => { - course_id => int($self->param('course_id')), + course_id => int($c->param('course_id')), } ); # Flatten to a single array. @@ -36,14 +36,14 @@ sub getCourseSettings ($self) { push(@course_settings, { var => $key, value => $course_settings->{$category}->{$key} }); } } - $self->render(json => \@course_settings); + $c->render(json => \@course_settings); return; } -sub updateCourseSetting ($self) { - my $course_setting = $self->schema->resultset('Course') - ->updateCourseSettings({ course_id => $self->param('course_id') }, $self->req->json); - $self->render(json => $course_setting); +sub updateCourseSetting ($c) { + my $course_setting = + $c->schema->resultset('Course')->updateCourseSettings({ course_id => $c->param('course_id') }, $c->req->json); + $c->render(json => $course_setting); return; } diff --git a/package.json b/package.json index 4780502e..f1272c2b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "lint": "eslint --ext .js,.ts,.vue . --fix && stylelint \"./src/**/*.vue\" \"./src/**/*.scss\" --fix", "test": "jest --testPathPattern='tests/unit-tests'", - "test-stores": "./bin/dev_scripts/test_pinia_stores", + "test-stores": "./bin/dev_scripts/test_pinia_stores.pl", "serve": "morbo bin/webwork3 & quasar dev", "build": "quasar build", "perltidy": "bash perltidy --pro=./.perltidyrc -b -bext='/' ./**/*.{t,pl,pm}" diff --git a/t/README.md b/t/README.md index 857892f4..75099ccf 100644 --- a/t/README.md +++ b/t/README.md @@ -1,38 +1,32 @@ # Tests for WeBWorK 3 -The subdirectories within this directory contain various tests for WeBWorK 3 including: +The subdirectories within this directory contain various tests for WeBWorK3 including: -* `db`: which directly tests the database. -* `mojolicious`: which tests the UI and api (REST services). +- `db`: which directly tests the database. +- `mojolicious`: which tests the UI and API (REST services). -## db subdirectory +To run all of the tests execute `prove -r t`. For more verbose output execute `prove -rv t`. Individual tests can be +run with `prove t/db/001_courses.t`, for example. -All of the database tests rely on a sqlite database file called `sample_db.sqlite`. -If this does not exist or perhaps needs to be rebuilt, run `perl build_db.pl`. This -will take data in CSV files in the `sample_data` directory and fill the database. +By default all tests are executed once with an in memory sqlite database. However, if the environment variable +`WW3_TEST_ALL_DBS` is set, then each test is executed three times, first with the in memory sqlite database, then with a +temporary postgres database instance, and then with a temporary mysql database instance. For example, execute +`WW3_TEST_ALL_DBS=1 prove -r t` or `WW3_TEST_ALL_DBS=1 prove -v t t/db/001_courses.t`. -All tests within the directory can be run with either `prove *.t` or `prove -lv *.t`, -where the first runs all tests within all test files and just reports a summary of -the results. The command with the `-lv` flags lists things test by test and any -output from the tests. +## db subdirectory -Additional, one can run an individual test as in the following example -`prove -lv 001_courses.t`. +Many of the database tests rely on data in JSON files in the `sample_data` directory. Each test populates the database +with the sample data it needs. ## mojolicious subdirectory -The tests in here use the testing ability built into Mojolicious, specifically -`Mojo::Test`. This spins up a mojolicious server and makes various server calls -and tests the results. +The tests in here use the testing ability built into Mojolicious, specifically `Mojo::Test`. This spins up a +Mojolicious server and makes various server calls and tests the results. -Like above the tests rely on the `sample_db.sqlite` database and it must be built -or rebuilt. +Many of these tests also rely on data in JSON files in the `sample_data` directory, and each test populates the database +with the sample data it needs. -Like the `db` subdirectory, all tests within the directory can be run with -either `prove *.t` or `prove -lv *.t`, -where the first runs all tests within all test files and just reports a summary of -the results. The command with the `-lv` flags lists things test by test and any -output from the tests. +## TODO -Also, one can run an individual test as in the following example -`prove -lv 001_login.t`. +1. Add new tests to individual files to ensure coverage. +2. Add new test files for new database functionality. diff --git a/t/db/001_courses.t b/t/db/001_courses.t index c5a5ffdf..9b35bc92 100644 --- a/t/db/001_courses.t +++ b/t/db/001_courses.t @@ -2,163 +2,201 @@ # This tests the basic database CRUD functions with courses. -use warnings; -use strict; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +use Test2::V0; -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -use Test2::V0; -use YAML::XS qw/LoadFile/; +use Mojo::File qw/curfile/; +use Test::PostgreSQL; use Mojo::JSON qw/true/; -use DB::Schema; - -use TestUtils qw/loadCSV removeIDs/; - -# Load the database -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -my $course_rs = $schema->resultset('Course'); - -# Get a list of courses from the CSV file. -my @courses = loadCSV( - "$main::ww3_dir/t/db/sample_data/courses.csv", - { - boolean_fields => ['visible'] - } -); - -@courses = sortByCourseName(\@courses); -for my $course (@courses) { - delete $course->{course_params}; -} - -# Check the list of all courses -my @courses_from_db = $course_rs->getCourses; -for my $course (@courses_from_db) { removeIDs($course); } -@courses_from_db = sortByCourseName(\@courses_from_db); - -is(\@courses_from_db, \@courses, 'getCourses: get all courses'); - -# Get a single course by name -my $course = $course_rs->getCourse(info => { course_name => 'Calculus' }); - -my $calc_id = $course->{course_id}; -delete $course->{course_id}; -my @calc_courses = grep { $_->{course_name} eq 'Calculus' } @courses; -is($course, $calc_courses[0], 'getCourse: get a single course by name'); - -# Get a single course by course_id -$course = $course_rs->getCourse(info => { course_id => $calc_id }); -delete $course->{course_id}; -is($course, $calc_courses[0], 'getCourse: get a single course by id'); - -# Try to get a single course by sending proper info: -is( - dies { $course_rs->getCourse(info => { course_id => $calc_id, course_name => 'Calculus' }); }, - check_isa('DB::Exception::ParametersNeeded'), - 'getCourse: sends too much info' -); - -is( - dies { $course_rs->getCourse(info => { name => 'Calculus' }); }, - check_isa('DB::Exception::ParametersNeeded'), - 'getCourse: sends wrong info' -); - -# Try to get a single course that doesn't exist -is( - dies { $course_rs->getCourse(info => { course_name => 'non_existent_course' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'getCourse: get a non-existent course' -); - -# Add a course -my $new_course_params = { course_name => 'Geometry', visible => true, course_dates => {} }; - -my $new_course = $course_rs->addCourse(params => $new_course_params); -my $added_course_id = $new_course->{course_id}; -removeIDs($new_course); - -is($new_course, $new_course_params, 'addCourse: add a new course'); - -# Add a course that already exists -is( - dies { $course_rs->addCourse(params => { course_name => 'Geometry', visible => true }); }, - check_isa('DB::Exception::CourseAlreadyExists'), - 'addCourse: course already exists' -); - -# Update the course name -my $updated_course = $course_rs->updateCourse( - info => { course_id => $added_course_id }, - params => { course_name => 'Geometry II' } -); - -$new_course_params->{course_name} = 'Geometry II'; -delete $updated_course->{course_id}; - -is($updated_course, $new_course_params, 'updateCourse: update a course by name'); - -# Try to update an non-existent course -is( - dies { $course_rs->updateCourse(info => { course_name => 'non_existent_course' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'updateCourse: update a non-existent course_name' -); - -is( - dies { $course_rs->updateCourse(info => { course_id => -9 }, params => $new_course_params); }, - check_isa('DB::Exception::CourseNotFound'), - 'updateCourse: update a non-existent course_id' -); - -# Delete a course -my $deleted_course = $course_rs->deleteCourse(info => { course_name => 'Geometry II' }); -removeIDs($deleted_course); - -is($deleted_course, $new_course_params, 'deleteCourse: delete a course'); - -# Try to delete a non-existent course by name -is( - dies { $course_rs->deleteCourse(info => { course_name => 'undefined_name' }) }, - check_isa('DB::Exception::CourseNotFound'), - 'deleteCourse: delete a non-existent course_name' -); - -# Try to delete a non-existent course by id -is( - dies { $course_rs->deleteCourse(info => { course_id => -9 }) }, - check_isa('DB::Exception::CourseNotFound'), - 'deleteCourse: delete a non-existent course_id' -); - -sub sortByCourseName { - my $course_rs = shift; - my @new_array = sort { $a->{course_name} cmp $b->{course_name} } @$course_rs; - return @new_array; -} - -# Check that the courses table is returned to its original state. -@courses_from_db = $course_rs->getCourses; -for my $course (@courses_from_db) { removeIDs($course); } -@courses_from_db = sortByCourseName(\@courses_from_db); - -is(\@courses_from_db, \@courses, 'check: courses db table is returned to its original state.'); +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use DBSubtest qw/dbSubtest/; + +dbSubtest courses => sub ($schema) { + my $course_rs = $schema->resultset('Course'); + + # Add a course + my $calc_course_params = + { course_name => 'Calculus', visible => true, course_dates => { start => '2020-09-01', end => '2020-12-16' } }; + my $calc_course = $course_rs->addCourse(params => $calc_course_params); + my $calc_course_id = $calc_course->{course_id}; + + is( + $calc_course, + hash { + field course_id => match qr/^\d*$/; + field course_name => $calc_course_params->{course_name}; + field visible => $calc_course_params->{visible}; + field course_dates => $calc_course_params->{course_dates}; + end; + }, + 'addCourse: add a new course' + ); + + # Add another course + my $geometry_course_params = { course_name => 'Geometry', visible => true, course_dates => {} }; + my $geometry_course = $course_rs->addCourse(params => $geometry_course_params); + + is( + $geometry_course, + hash { + field course_id => match qr/^\d*$/; + field course_name => $geometry_course_params->{course_name}; + field visible => $geometry_course_params->{visible}; + field course_dates => $geometry_course_params->{course_dates}; + end; + }, + 'addCourse: add another new course' + ); + + # Retrive the list of all courses now in the database and check it is correct. + my @courses_from_db = $course_rs->getCourses; + @courses_from_db = sort { $a->{course_name} cmp $b->{course_name} } @courses_from_db; + + is( + \@courses_from_db, + array { + item 0 => hash { + field course_id => match qr/^\d*$/; + field course_name => $calc_course_params->{course_name}; + field visible => $calc_course_params->{visible}; + field course_dates => $calc_course_params->{course_dates}; + end; + }; + item 1 => hash { + field course_id => match qr/^\d*$/; + field course_name => $geometry_course_params->{course_name}; + field visible => $geometry_course_params->{visible}; + field course_dates => $geometry_course_params->{course_dates}; + end; + }; + end; + }, + 'getCourses: get all courses' + ); + + # Get a single course by name + my $course = $course_rs->getCourse(info => { course_name => 'Calculus' }); + + is( + $course, + hash { + field course_id => match qr/^\d*$/; + field course_name => $calc_course_params->{course_name}; + field visible => $calc_course_params->{visible}; + field course_dates => $calc_course_params->{course_dates}; + end; + }, + 'getCourse: get a single course by name' + ); + + # Get a single course by course_id + $course = $course_rs->getCourse(info => { course_id => $calc_course_id }); + is( + $course, + hash { + field course_id => match qr/^\d*$/; + field course_name => $calc_course_params->{course_name}; + field visible => $calc_course_params->{visible}; + field course_dates => $calc_course_params->{course_dates}; + end; + }, + 'getCourse: get a single course by id' + ); + + # Try to get a single course with invalid information. + is( + dies { $course_rs->getCourse(info => { course_id => $calc_course_id, course_name => 'Calculus' }); } + , + check_isa('DB::Exception::ParametersNeeded'), + 'getCourse: sends too much info' + ); + + is( + dies { $course_rs->getCourse(info => { name => 'Calculus' }); }, + check_isa('DB::Exception::ParametersNeeded'), + 'getCourse: sends wrong info' + ); + + # Try to get a single course that doesn't exist + is( + dies { $course_rs->getCourse(info => { course_name => 'non_existent_course' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getCourse: get a non-existent course' + ); + + # Add a course that already exists + is( + dies { $course_rs->addCourse(params => { course_name => 'Geometry', visible => true }); }, + check_isa('DB::Exception::CourseAlreadyExists'), + 'addCourse: course already exists' + ); + + # Update the course name + my $updated_course = $course_rs->updateCourse( + info => { course_id => $geometry_course->{course_id} }, + params => { course_name => 'Geometry II' } + ); + + $geometry_course_params->{course_name} = 'Geometry II'; + + is( + $updated_course, + hash { + field course_id => match qr/^\d*$/; + field course_name => $geometry_course_params->{course_name}; + field visible => $geometry_course_params->{visible}; + field course_dates => $geometry_course_params->{course_dates}; + end; + }, + 'updateCourse: update a course by name' + ); + + # Try to update an non-existent course + is( + dies { $course_rs->updateCourse(info => { course_name => 'non_existent_course' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'updateCourse: update a non-existent course_name' + ); + + is( + dies { $course_rs->updateCourse(info => { course_id => -9 }, params => $geometry_course_params); }, + check_isa('DB::Exception::CourseNotFound'), + 'updateCourse: update a non-existent course_id' + ); + + # Delete a course + my $deleted_course = $course_rs->deleteCourse(info => { course_name => 'Geometry II' }); + + is( + $deleted_course, + hash { + field course_id => match qr/^\d*$/; + field course_name => $geometry_course_params->{course_name}; + field visible => $geometry_course_params->{visible}; + field course_dates => $geometry_course_params->{course_dates}; + end; + }, + 'deleteCourse: delete a course' + ); + + # Try to delete a non-existent course by name + is( + dies { $course_rs->deleteCourse(info => { course_name => 'undefined_name' }) }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteCourse: delete a non-existent course_name' + ); + + # Try to delete a non-existent course by id + is( + dies { $course_rs->deleteCourse(info => { course_id => -9 }) }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteCourse: delete a non-existent course_id' + ); +}; done_testing(); diff --git a/t/db/002_course_settings.t b/t/db/002_course_settings.t index 1f5a299e..928c5922 100644 --- a/t/db/002_course_settings.t +++ b/t/db/002_course_settings.t @@ -2,285 +2,275 @@ # This tests the basic database CRUD functions with course settings. -use warnings; -use strict; +use Test2::V0; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; +use Mojo::File qw/curfile/; -use Test2::V0; -use YAML::XS qw/LoadFile/; +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; -use DB::Schema; +use DBSubtest qw/dbSubtest/; -use WeBWorK3::Utils::Settings qw/getDefaultCourseSettings getDefaultCourseValues - validateSettingsConfFile validateSettingConfig +use WeBWorK3::Utils::Settings qw/getDefaultCourseValues validateSettingsConfFile validateSettingConfig isInteger isTimeString isTimeDuration isDecimal mergeCourseSettings/; -use TestUtils qw/removeIDs loadSchema/; - -# Load the database -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - # Test for various types - -ok(isInteger(0), 'check type: integer'); -ok(isInteger(100), 'check type: integer'); -ok(isInteger(-30), 'check type: integer'); -ok(!isInteger(0.5), 'check type: not an integer'); -ok(!isInteger(-2.5), 'check type: not an integer'); - -ok(isTimeString('1:59'), 'check type: 24-hour time string'); -ok(isTimeString('01:59'), 'check type: 24-hour time string'); -ok(isTimeString('00:59'), 'check type: 24-hour time string'); -ok(isTimeString('11:59'), 'check type: 24-hour time string'); -ok(isTimeString('13:59'), 'check type: 24-hour time string'); -ok(isTimeString('21:00'), 'check type: 24-hour time string'); - -ok(!isTimeString('24:59'), 'check type: not a 24-hour time string'); -ok(!isTimeString('31:59'), 'check type: not a 24-hour time string'); -ok(!isTimeString('23:69'), 'check type: not a 24-hour time string'); - -ok(isTimeDuration('2 days'), 'check type: time duration'); -ok(isTimeDuration('10 sec'), 'check type: time duration'); -ok(isTimeDuration('12 min'), 'check type: time duration'); -ok(isTimeDuration('24 hrs'), 'check type: time duration'); -ok(isTimeDuration('24 hours'), 'check type: time duration'); -ok(isTimeDuration('1 week'), 'check type: time duration'); -ok(isTimeDuration('1 WEEK'), 'check type: time duration'); - -ok(!isTimeDuration('-24 hrs'), 'check type: not a time duration'); -ok(!isTimeDuration('4 apples'), 'check type: not a time duration'); - -ok(isDecimal(0.3), 'check type: decimal'); -ok(isDecimal(3), 'check type: decimal'); -ok(isDecimal(-0.3), 'check type: decimal'); -ok(isDecimal('0.33'), 'check type: decimal'); -ok(isDecimal("-.33"), 'check type: decimal'); - -ok(isDecimal('00.33'), 'check type: decimal'); -ok(!isDecimal("0-.33"), 'check type: not a decimal'); -ok(!isDecimal('abc'), 'check type: not a decimal'); +subtest 'check types' => sub { + ok(isInteger(0), 'check type: integer'); + ok(isInteger(100), 'check type: integer'); + ok(isInteger(-30), 'check type: integer'); + ok(!isInteger(0.5), 'check type: not an integer'); + ok(!isInteger(-2.5), 'check type: not an integer'); + + ok(isTimeString('1:59'), 'check type: 24-hour time string'); + ok(isTimeString('01:59'), 'check type: 24-hour time string'); + ok(isTimeString('00:59'), 'check type: 24-hour time string'); + ok(isTimeString('11:59'), 'check type: 24-hour time string'); + ok(isTimeString('13:59'), 'check type: 24-hour time string'); + ok(isTimeString('21:00'), 'check type: 24-hour time string'); + + ok(!isTimeString('24:59'), 'check type: not a 24-hour time string'); + ok(!isTimeString('31:59'), 'check type: not a 24-hour time string'); + ok(!isTimeString('23:69'), 'check type: not a 24-hour time string'); + + ok(isTimeDuration('2 days'), 'check type: time duration'); + ok(isTimeDuration('10 sec'), 'check type: time duration'); + ok(isTimeDuration('12 min'), 'check type: time duration'); + ok(isTimeDuration('24 hrs'), 'check type: time duration'); + ok(isTimeDuration('24 hours'), 'check type: time duration'); + ok(isTimeDuration('1 week'), 'check type: time duration'); + ok(isTimeDuration('1 WEEK'), 'check type: time duration'); + + ok(!isTimeDuration('-24 hrs'), 'check type: not a time duration'); + ok(!isTimeDuration('4 apples'), 'check type: not a time duration'); + + ok(isDecimal(0.3), 'check type: decimal'); + ok(isDecimal(3), 'check type: decimal'); + ok(isDecimal(-0.3), 'check type: decimal'); + ok(isDecimal('0.33'), 'check type: decimal'); + ok(isDecimal('-.33'), 'check type: decimal'); + + ok(isDecimal('00.33'), 'check type: decimal'); + ok(!isDecimal('0-.33'), 'check type: not a decimal'); + ok(!isDecimal('abc'), 'check type: not a decimal'); +}; # Check that the configuration file is valid. -is(validateSettingsConfFile(), 1, 'configuration file valid'); +is(validateSettingsConfFile, 1, 'configuration file valid'); # TODO: Test to make sure that all of the checks for the course configurations work. -my $default_course_settings = getDefaultCourseSettings(); +subtest 'check setting validation' => sub { -# Check that each of the given course_setting types are both valid and invalid. -my $valid_setting = { - var => 'my_setting', - doc => 'this is a setting', - type => 'integer', - category => 'general', - default => 0 -}; -is(validateSettingConfig($valid_setting), 1, 'course setting: valid setting'); - -# Check various parts of the setting. - -is( - dies { - validateSettingConfig({ - var => 'mySetting', - doc => 'this is a setting', - type => 'integer', - category => 'general', - default => 0 - }) - }, - check_isa('DB::Exception::InvalidCourseField'), - 'course setting: variable not in kebob case' -); - -is( - dies { - validateSettingConfig({ - var => 'my_setting', - doc3 => 'this is a setting', - type => 'integer', - category => 'general', - default => 0 - }) - }, - check_isa('DB::Exception::InvalidCourseField'), - 'course setting: course setting with illegal field' -); - -is( - dies { - validateSettingConfig({ - var => 'my_setting', - type => 'integer', - category => 'general', - default => 0 - }) - }, - check_isa('DB::Exception::InvalidCourseField'), - 'course setting: missing required field' -); - -is( - dies { + # Check that each of the given course_setting types are both valid and invalid. + is( validateSettingConfig({ var => 'my_setting', doc => 'this is a setting', - type => 'nonnegint', + type => 'integer', category => 'general', default => 0 - }) - }, - check_isa('DB::Exception::InvalidCourseFieldType'), - 'course setting: non valid course parameter type' -); - -# Validate settings + }), + 1, + 'course setting: valid setting' + ); + + # Check various parts of the setting. + + is( + dies { + validateSettingConfig({ + var => 'mySetting', + doc => 'this is a setting', + type => 'integer', + category => 'general', + default => 0 + }) + }, + check_isa('DB::Exception::InvalidCourseField'), + 'course setting: variable not in kebob case' + ); + + is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc3 => 'this is a setting', + type => 'integer', + category => 'general', + default => 0 + }) + }, + check_isa('DB::Exception::InvalidCourseField'), + 'course setting: course setting with illegal field' + ); + + is( + dies { + validateSettingConfig({ + var => 'my_setting', + type => 'integer', + category => 'general', + default => 0 + }) + }, + check_isa('DB::Exception::InvalidCourseField'), + 'course setting: missing required field' + ); + + is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc => 'this is a setting', + type => 'nonnegint', + category => 'general', + default => 0 + }) + }, + check_isa('DB::Exception::InvalidCourseFieldType'), + 'course setting: non valid course parameter type' + ); + + # Validate settings + + is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc => 'this is a setting', + type => 'time', + category => 'general', + default => '12:343' + }) + }, + check_isa('DB::Exception::InvalidCourseFieldType'), + 'course setting: bad time string' + ); + + is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc => 'this is a setting', + type => 'integer', + category => 'general', + default => '12.343' + }) + }, + check_isa('DB::Exception::InvalidCourseFieldType'), + 'course setting: bad integer format' + ); + + is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc => 'this is a setting', + type => 'time_duration', + category => 'general', + default => '-2 days' + }) + }, + check_isa('DB::Exception::InvalidCourseFieldType'), + 'course setting: bad time duration format' + ); + + is( + dies { + validateSettingConfig({ + var => 'my_setting', + doc => 'this is a setting', + type => 'decimal', + category => 'general', + default => '12:343' + }) + }, + check_isa('DB::Exception::InvalidCourseFieldType'), + 'course setting: bad decimal format' + ); +}; -is( - dies { - validateSettingConfig({ - var => 'my_setting', - doc => 'this is a setting', - type => 'time', - category => 'general', - default => '12:343' - }) - }, - check_isa('DB::Exception::InvalidCourseFieldType'), - 'course setting: bad time string' -); - -is( - dies { - validateSettingConfig({ - var => 'my_setting', - doc => 'this is a setting', - type => 'integer', - category => 'general', - default => '12.343' - }) - }, - check_isa('DB::Exception::InvalidCourseFieldType'), - 'course setting: bad integer format' -); - -is( - dies { - validateSettingConfig({ - var => 'my_setting', - doc => 'this is a setting', - type => 'time_duration', - category => 'general', - default => '-2 days' - }) - }, - check_isa('DB::Exception::InvalidCourseFieldType'), - 'course setting: bad time duration format' -); - -is( - dies { - validateSettingConfig({ - var => 'my_setting', - doc => 'this is a setting', - type => 'decimal', - category => 'general', - default => '12:343' - }) - }, - check_isa('DB::Exception::InvalidCourseFieldType'), - 'course setting: bad decimal format' -); - -my $course_rs = $schema->resultset('Course'); - -# Check that the default settings are working - -# Make a new course with no settings and compare to the default settings -my $new_course = $course_rs->addCourse(params => { course_name => 'New Course' }); - -my $default_course_values = getDefaultCourseValues(); -my $new_course_info = { course_id => $new_course->{course_id} }; -my $course_settings = $course_rs->getCourseSettings(info => $new_course_info); - -is($course_settings, $default_course_values, 'course settings: default course_settings'); - -# Set a single course setting in General -my $updated_general_setting = { general => { course_description => 'This is my new course description' } }; -my $updated_course_settings = $course_rs->updateCourseSettings( - info => $new_course_info, - settings => $updated_general_setting -); -my $current_course_values = mergeCourseSettings($default_course_values, $updated_general_setting); - -is($current_course_values, $updated_course_settings, 'course_settings: updated general setting'); - -# Update another general setting -$updated_general_setting = { general => { hardcopy_theme => 'One Column' } }; - -$updated_course_settings = $course_rs->updateCourseSettings( - info => $new_course_info, - settings => $updated_general_setting -); - -$current_course_values = mergeCourseSettings($current_course_values, $updated_general_setting); - -is($current_course_values, $updated_course_settings, 'course_settings: updated another general setting'); - -# Set a single course setting in Optional Modules. -my $updated_optional_setting = { optional => { enable_show_me_another => 1 } }; -$updated_course_settings = - $course_rs->updateCourseSettings(info => $new_course_info, settings => $updated_optional_setting); -$current_course_values = mergeCourseSettings($current_course_values, $updated_optional_setting); -is($current_course_values, $updated_course_settings, 'course_settings: updated optional setting'); - -# Set a single course setting in problem_set. -my $updated_problem_set_setting = { problem_set => { time_assign_due => '11:52' } }; -$updated_course_settings = - $course_rs->updateCourseSettings(info => $new_course_info, settings => $updated_problem_set_setting); -$current_course_values = mergeCourseSettings($current_course_values, $updated_problem_set_setting); -is($current_course_values, $updated_course_settings, 'course_settings: updated problem set setting'); - -# Set a single course setting in problem. -my $updated_problem_setting = { problem => { display_mode => 'images' } }; -$updated_course_settings = - $course_rs->updateCourseSettings(info => $new_course_info, settings => $updated_problem_setting); -$current_course_values = mergeCourseSettings($current_course_values, $updated_problem_setting); -is($current_course_values, $updated_course_settings, 'course_settings: updated problem setting'); - -# Make sure that an nonexistant setting throws an exception. -my $undefined_problem_setting = { general => { non_existent_setting => 1 } }; -is( - dies { $course_rs->updateCourseSettings(info => $new_course_info, settings => $undefined_problem_setting); }, - check_isa('DB::Exception::UndefinedCourseField'), - 'course settings: undefined course_setting field' -); - -# Make sure that an invalid list option setting throws an exception. -my $invalid_list_option = { general => { hardcopy_theme => 'default' } }; -$course_rs->updateCourseSettings(info => $new_course_info, settings => $invalid_list_option); - -# TODO: Make sure that an invalid integer setting throws an exception - -# TODO: Make sure that an invalid email list setting throws an exception - -# Finally delete the course that was made -$course_rs->deleteCourse(info => { course_id => $new_course->{course_id} }); +dbSubtest 'course settings' => sub ($schema) { + my $course_rs = $schema->resultset('Course'); + + # Check that the default settings are working + + # Make a new course with no settings and compare to the default settings + my $new_course = $course_rs->addCourse(params => { course_name => 'New Course' }); + + my $default_course_values = getDefaultCourseValues(); + my $new_course_info = { course_id => $new_course->{course_id} }; + my $course_settings = $course_rs->getCourseSettings(info => $new_course_info); + + is($course_settings, $default_course_values, 'course settings: default course_settings'); + + # Set a single course setting in General + my $updated_general_setting = { general => { course_description => 'This is my new course description' } }; + my $updated_course_settings = $course_rs->updateCourseSettings( + info => $new_course_info, + settings => $updated_general_setting + ); + my $current_course_values = mergeCourseSettings($default_course_values, $updated_general_setting); + + is($current_course_values, $updated_course_settings, 'course_settings: updated general setting'); + + # Update another general setting + $updated_general_setting = { general => { hardcopy_theme => 'One Column' } }; + + $updated_course_settings = $course_rs->updateCourseSettings( + info => $new_course_info, + settings => $updated_general_setting + ); + + $current_course_values = mergeCourseSettings($current_course_values, $updated_general_setting); + + is($current_course_values, $updated_course_settings, 'course_settings: updated another general setting'); + + # Set a single course setting in Optional Modules. + my $updated_optional_setting = { optional => { enable_show_me_another => 1 } }; + $updated_course_settings = + $course_rs->updateCourseSettings(info => $new_course_info, settings => $updated_optional_setting); + $current_course_values = mergeCourseSettings($current_course_values, $updated_optional_setting); + is($current_course_values, $updated_course_settings, 'course_settings: updated optional setting'); + + # Set a single course setting in problem_set. + my $updated_problem_set_setting = { problem_set => { time_assign_due => '11:52' } }; + $updated_course_settings = + $course_rs->updateCourseSettings(info => $new_course_info, settings => $updated_problem_set_setting); + $current_course_values = mergeCourseSettings($current_course_values, $updated_problem_set_setting); + is($current_course_values, $updated_course_settings, 'course_settings: updated problem set setting'); + + # Set a single course setting in problem. + my $updated_problem_setting = { problem => { display_mode => 'images' } }; + $updated_course_settings = + $course_rs->updateCourseSettings(info => $new_course_info, settings => $updated_problem_setting); + $current_course_values = mergeCourseSettings($current_course_values, $updated_problem_setting); + is($current_course_values, $updated_course_settings, 'course_settings: updated problem setting'); + + # Make sure that an nonexistant setting throws an exception. + my $undefined_problem_setting = { general => { non_existent_setting => 1 } }; + is( + dies { + $course_rs->updateCourseSettings( + info => $new_course_info, + settings => $undefined_problem_setting + ); + }, + check_isa('DB::Exception::UndefinedCourseField'), + 'course settings: undefined course_setting field' + ); + + # Make sure that an invalid list option setting throws an exception. + my $invalid_list_option = { general => { hardcopy_theme => 'default' } }; + $course_rs->updateCourseSettings(info => $new_course_info, settings => $invalid_list_option); + + # TODO: Make sure that an invalid integer setting throws an exception + + # TODO: Make sure that an invalid email list setting throws an exception +}; done_testing(); diff --git a/t/db/003_users.t b/t/db/003_users.t index ce646ab2..147c5714 100644 --- a/t/db/003_users.t +++ b/t/db/003_users.t @@ -2,300 +2,269 @@ # This tests the basic database CRUD functions with courses. -use warnings; -use strict; +use Test2::V0; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json true false/; -use Test2::V0; -use Clone qw/clone/; - -use YAML qw/LoadFile/; -use Mojo::JSON qw/true false/; - -use DB::Schema; -use TestUtils qw/loadCSV removeIDs cleanUndef/; - -# Load the database -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -my $users_rs = $schema->resultset('User'); -my $course_rs = $schema->resultset('Course'); - -# Get a list of users from the CSV file -my @students = loadCSV("$main::ww3_dir/t/db/sample_data/students.csv"); - -# Remove duplicates -my %seen = (); -@students = grep { !$seen{ $_->{username} }++ } @students; -for my $student (@students) { - for my $key (qw/course_name recitation section params role/) { - delete $student->{$key}; - } - cleanUndef($student); - $student->{is_admin} = false; -} - -# Add the admin user -push( - @students, - { - username => 'admin', - email => 'admin@google.com', - is_admin => true, - first_name => 'Andrea', - last_name => 'Administrator', - } -); -my @all_students = sort { $a->{username} cmp $b->{username} } @students; +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; -# Get a list of all users. -my @users_from_db = $users_rs->getAllGlobalUsers; -for my $user (@users_from_db) { - removeIDs($user); - cleanUndef($user); -} -@users_from_db = sort { $a->{username} cmp $b->{username} } @users_from_db; -is(\@users_from_db, \@all_students, 'getUsers: all users'); - -# Get a single user by username -my $user = $users_rs->getGlobalUser(info => { username => $all_students[0]->{username} }); -removeIDs($user); -cleanUndef($user); -is($user, $all_students[0], 'getUser: by username'); - -# Get a single user by user_id -$user = $users_rs->getGlobalUser(info => { user_id => 2 }); -removeIDs($user); -my @stud2 = grep { $_->{username} eq $user->{username} } @all_students; -is($user, $stud2[0], 'getUser: by user_id'); - -# Get one user that does not exist -is( - dies { $user = $users_rs->getGlobalUser(info => { user_id => -9 }); }, - check_isa('DB::Exception::UserNotFound'), - 'getUser: undefined user_id' -); - -is( - dies { $user = $users_rs->getGlobalUser(info => { username => 'non_existent_user' }); }, - check_isa('DB::Exception::UserNotFound'), - 'getUser: undefined username' -); - -# getUsers: Test that not passing either a course_id or course_name results in an error. -is( - dies { $users_rs->getCourseUsers(info => { my_course => 'Precalculus' }); }, - check_isa('DB::Exception::ParametersNeeded'), - 'getUsers: course_name or course_id not passed in' -); - -# Add one user -$user = { - username => 'wiggam', - last_name => 'Wiggam', - first_name => 'Clancy', - email => 'wiggam@springfieldpd.gov', - student_id => '', - is_admin => false, -}; +use BuildDB qw/loadPermissions addCourses addUsers/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs cleanUndef/; -my $new_user = $users_rs->addGlobalUser(params => $user); -removeIDs($new_user); -is($new_user, $user, 'addUser: adding a user'); - -# Ensure that the default values are set -my $patty_params = { username => 'patty' }; -my $patty = $users_rs->addGlobalUser(params => $patty_params); -removeIDs($patty); -cleanUndef($patty); -# the only default for users is { is_admin: false } -$patty_params->{is_admin} = false; -is($patty, $patty_params, 'addUser: check the default values from db.'); - -# Try to add a user without passing username info -is( - dies { $users_rs->addGlobalUser(params => { username_name => 'selma', email => 'selma@google.com' }); }, - check_isa('DB::Exception::ParametersNeeded'), - 'addUser: wrong user_info sent' -); - -# Check that adding an invalid field ignores that field. - -my $selma_params = { - username => 'selma', - invalid_field => 1 -}; +my $ww3_dir = curfile->dirname->dirname->dirname; -my $selma = $users_rs->addGlobalUser(params => $selma_params); -removeIDs($selma); -cleanUndef($selma); - -# cleanup params for comparison: invalid_field dropped, is_admin matches default -delete $selma_params->{invalid_field}; -$selma_params->{is_admin} = false; -is($selma, $selma_params, 'addUser: pass in an invalid field'); - -# Add a user with an invalid username -is( - dies { $users_rs->addGlobalUser(params => { username => 'my name is selma', email => 'selma@google.com' }); }, - check_isa('DB::Exception::InvalidParameter'), - 'addUser: bad username sent' -); - -# Check that using an email address for a username is valid: -# Add one user -my $user2 = { - username => 'selma@google.com', - last_name => 'Bouvier', - first_name => 'Selma', - email => 'selma@google.com', - student_id => '', - is_admin => false, -}; +dbSubtest users => sub ($schema) { + # Add the neccessary sample data to the database. + loadPermissions($schema, $ww3_dir); + addCourses($schema, $ww3_dir); + addUsers($schema, $ww3_dir); + + my $users_rs = $schema->resultset('User'); + my $course_rs = $schema->resultset('Course'); + + # Get a list of users from the JSON file + my $users = decode_json($ww3_dir->child('t/db/sample_data/users.json')->slurp); -my $added_user2 = $users_rs->addGlobalUser(params => $user2); -removeIDs($added_user2); -is($added_user2, $user2, 'addUser: check that using an email for a username is valid.'); - -# Update a user -my $updated_user = clone $user; -$updated_user->{email} = 'spring.cop@gmail.com'; -my $updated_user_from_db = $users_rs->updateGlobalUser( - info => { username => $updated_user->{username} }, - params => $updated_user -); -removeIDs($updated_user_from_db); -is($updated_user_from_db, $updated_user, 'updateUser: updating a user'); - -# Try to update a user without passing username info -is( - dies { $users_rs->updateGlobalUser(info => { username_name => 'wiggam' }, params => $updated_user); }, - check_isa('DB::Exception::ParametersNeeded'), - 'updateUser: wrong user_info sent' -); - -# Try to update a user that doesn't exist -is( - dies { $users_rs->updateGlobalUser(info => { username => 'non_existent_user' }, params => $updated_user); }, - check_isa('DB::Exception::UserNotFound'), - 'updateUser: update user for a non-existing username' -); - -is( - dies { $users_rs->updateGlobalUser(info => { user_id => -5 }, params => $updated_user); }, - check_isa('DB::Exception::UserNotFound'), - 'updateUser: update user for a non-existing user_id' -); - -# Check that updated an invalid field throws an error -like( - dies { $users_rs->updateGlobalUser(info => { username => 'wiggam' }, params => { invalid_field => 1 }) }, - qr/No such column 'invalid_field'/, - 'updateUser: pass in an invalid field' -); - -# Delete users that were created -my $user_to_delete = $users_rs->deleteGlobalUser(info => { username => $user->{username} }); - -removeIDs($user_to_delete); -cleanUndef($user_to_delete); -is($user_to_delete, $updated_user, 'deleteUser: delete a user'); - -my $deleted_selma = $users_rs->deleteGlobalUser(info => { username => 'selma' }); -removeIDs($deleted_selma); -cleanUndef($deleted_selma); -is($deleted_selma, $selma_params, 'deleteUser: deleter another user'); - -my $deleted_patty = $users_rs->deleteGlobalUser(info => { username => 'patty' }); -removeIDs($deleted_patty); -cleanUndef($deleted_patty); -is($deleted_patty, $patty_params, 'deleteUser: deleter a third user'); - -my $user_to_delete2 = $users_rs->deleteGlobalUser(info => { username => $added_user2->{username} }); -removeIDs($user_to_delete2); -cleanUndef($user_to_delete2); -is($user_to_delete2, $added_user2, 'deleteUser: delete yet another user.'); - -# Delete a user that doesn't exist. -is( - dies { $user = $users_rs->deleteGlobalUser(info => { username => 'undefined_username' }); }, - check_isa('DB::Exception::UserNotFound'), - 'deleteUser: trying to delete with undefined username' -); - -is( - dies { $user = $users_rs->deleteGlobalUser(info => { user_id => -3 }); }, - check_isa('DB::Exception::UserNotFound'), - 'deleteUser: trying to delete with undefined user_id' -); - -# get a list of courses for a user - -my @user_courses = $course_rs->getUserCourses(info => { username => 'lisa' }); -for my $user_course (@user_courses) { - removeIDs($user_course); - cleanUndef($user_course); -} - -my @courses = loadCSV( - "$main::ww3_dir/t/db/sample_data/courses.csv", - { - boolean_fields => ['visible'] + my @users; + for my $user (@$users) { + # Copy the users so the users.json file does not need to be read again. + my $user_copy = {%$user}; + delete $user_copy->{courses}; + $user_copy->{is_admin} //= false; + push(@users, $user_copy); } -); -@students = loadCSV("$main::ww3_dir/t/db/sample_data/students.csv"); -my @user_courses_from_csv = grep { $_->{username} eq 'lisa' } @students; + @users = sort { $a->{username} cmp $b->{username} } @users; -for my $user_course (@user_courses_from_csv) { - my $course = (grep { $_->{course_name} eq $user_course->{course_name} } @courses)[0]; - for my $key (qw/email first_name last_name username student_id/) { - delete $user_course->{$key}; + # Get a list of all users. + my @users_from_db = $users_rs->getAllGlobalUsers; + for my $user (@users_from_db) { + removeIDs($user); + cleanUndef($user); } - cleanUndef($user_course); - $user_course->{course_user_params} = $user_course->{params}; - delete $user_course->{params}; - $user_course->{visible} = $course->{visible}; - $user_course->{course_dates} = $course->{course_dates}; -} - -# Make sure that the order of the courses is the same -@user_courses_from_csv = sort { $a->{course_name} cmp $b->{course_name} } @user_courses_from_csv; -@user_courses = sort { $a->{course_name} cmp $b->{course_name} } @user_courses; - -is(\@user_courses, \@user_courses_from_csv, 'getUserCourses: get all courses for a given user'); - -# try to get a list of course from a non-existent user - -is( - dies { $course_rs->getUserCourses(info => { username => 'non_existent_user' }); }, - check_isa('DB::Exception::UserNotFound'), - 'getUserCourses: try to get a list of courses for a non-existent user' -); - -# Check that the users db table is returned to its original state. -@users_from_db = $users_rs->getAllGlobalUsers; -for my $user (@users_from_db) { + @users_from_db = sort { $a->{username} cmp $b->{username} } @users_from_db; + is(\@users_from_db, \@users, 'getUsers: all users'); + + # Get a single user by username + my $user = $users_rs->getGlobalUser(info => { username => $users[0]->{username} }); removeIDs($user); cleanUndef($user); -} -@users_from_db = sort { $a->{username} cmp $b->{username} } @users_from_db; -is(\@users_from_db, \@all_students, 'check: make sure that the users db table is returned to its original state'); + is($user, $users[0], 'getUser: by username'); + + # Get a single user by user_id + $user = $users_rs->getGlobalUser(info => { user_id => 2 }); + removeIDs($user); + my @stud2 = grep { $_->{username} eq $user->{username} } @users; + is($user, $stud2[0], 'getUser: by user_id'); + + # Get one user that does not exist + is( + dies { $user = $users_rs->getGlobalUser(info => { user_id => -9 }); }, + check_isa('DB::Exception::UserNotFound'), + 'getUser: undefined user_id' + ); + + is( + dies { $user = $users_rs->getGlobalUser(info => { username => 'non_existent_user' }); }, + check_isa('DB::Exception::UserNotFound'), + 'getUser: undefined username' + ); + + # getUsers: Test that not passing either a course_id or course_name results in an error. + is( + dies { $users_rs->getCourseUsers(info => { my_course => 'Precalculus' }); }, + check_isa('DB::Exception::ParametersNeeded'), + 'getUsers: course_name or course_id not passed in' + ); + + # Add one user + $user = { + username => 'wiggam', + last_name => 'Wiggam', + first_name => 'Clancy', + email => 'wiggam@springfieldpd.gov', + student_id => '', + is_admin => false, + }; + + my $new_user = $users_rs->addGlobalUser(params => $user); + removeIDs($new_user); + is($new_user, $user, 'addUser: adding a user'); + + # Ensure that the default values are set + my $patty_params = { username => 'patty' }; + my $patty = $users_rs->addGlobalUser(params => $patty_params); + removeIDs($patty); + cleanUndef($patty); + # the only default for users is { is_admin: false } + $patty_params->{is_admin} = false; + is($patty, $patty_params, 'addUser: check the default values from db.'); + + # Try to add a user without passing username info + is( + dies { + $users_rs->addGlobalUser(params => { username_name => 'selma', email => 'selma@google.com' }); + }, + check_isa('DB::Exception::ParametersNeeded'), + 'addUser: wrong user_info sent' + ); + + # Check that adding an invalid field ignores that field. + + my $selma_params = { username => 'selma', invalid_field => 1 }; + + my $selma = $users_rs->addGlobalUser(params => $selma_params); + removeIDs($selma); + cleanUndef($selma); + + # cleanup params for comparison: invalid_field dropped, is_admin matches default + delete $selma_params->{invalid_field}; + $selma_params->{is_admin} = false; + is($selma, $selma_params, 'addUser: pass in an invalid field'); + + # Add a user with an invalid username + is( + dies { + $users_rs->addGlobalUser(params => { username => 'my name is selma', email => 'selma@google.com' }); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addUser: bad username sent' + ); + + # Check that using an email address for a username is valid: + # Add one user + my $user2 = { + username => 'selma@google.com', + last_name => 'Bouvier', + first_name => 'Selma', + email => 'selma@google.com', + student_id => '', + is_admin => false, + }; + + my $added_user2 = $users_rs->addGlobalUser(params => $user2); + removeIDs($added_user2); + is($added_user2, $user2, 'addUser: check that using an email for a username is valid.'); + + # Update a user + my $updated_user = {%$user}; + $updated_user->{email} = 'spring.cop@gmail.com'; + my $updated_user_from_db = $users_rs->updateGlobalUser( + info => { username => $updated_user->{username} }, + params => $updated_user + ); + removeIDs($updated_user_from_db); + is($updated_user_from_db, $updated_user, 'updateUser: updating a user'); + + # Try to update a user without passing username info + is( + dies { + $users_rs->updateGlobalUser(info => { username_name => 'wiggam' }, params => $updated_user); + }, + check_isa('DB::Exception::ParametersNeeded'), + 'updateUser: wrong user_info sent' + ); + + # Try to update a user that doesn't exist + is( + dies { + $users_rs->updateGlobalUser( + info => { username => 'non_existent_user' }, + params => $updated_user + ); + }, + check_isa('DB::Exception::UserNotFound'), + 'updateUser: update user for a non-existing username' + ); + + is( + dies { $users_rs->updateGlobalUser(info => { user_id => -5 }, params => $updated_user); }, + check_isa('DB::Exception::UserNotFound'), + 'updateUser: update user for a non-existing user_id' + ); + + # Check that updated an invalid field throws an error + like( + dies { + $users_rs->updateGlobalUser(info => { username => 'wiggam' }, params => { invalid_field => 1 }) + }, + qr/No such column 'invalid_field'/, + 'updateUser: pass in an invalid field' + ); + + # Delete users that were created + my $user_to_delete = $users_rs->deleteGlobalUser(info => { username => $user->{username} }); + + removeIDs($user_to_delete); + cleanUndef($user_to_delete); + is($user_to_delete, $updated_user, 'deleteUser: delete a user'); + + my $deleted_selma = $users_rs->deleteGlobalUser(info => { username => 'selma' }); + removeIDs($deleted_selma); + cleanUndef($deleted_selma); + is($deleted_selma, $selma_params, 'deleteUser: deleter another user'); + + my $deleted_patty = $users_rs->deleteGlobalUser(info => { username => 'patty' }); + removeIDs($deleted_patty); + cleanUndef($deleted_patty); + is($deleted_patty, $patty_params, 'deleteUser: deleter a third user'); + + my $user_to_delete2 = $users_rs->deleteGlobalUser(info => { username => $added_user2->{username} }); + removeIDs($user_to_delete2); + cleanUndef($user_to_delete2); + is($user_to_delete2, $added_user2, 'deleteUser: delete yet another user.'); + + # Delete a user that doesn't exist. + is( + dies { $user = $users_rs->deleteGlobalUser(info => { username => 'undefined_username' }); }, + check_isa('DB::Exception::UserNotFound'), + 'deleteUser: trying to delete with undefined username' + ); + + is( + dies { $user = $users_rs->deleteGlobalUser(info => { user_id => -3 }); }, + check_isa('DB::Exception::UserNotFound'), + 'deleteUser: trying to delete with undefined user_id' + ); + + # Get the list of courses for a user + my @user_courses = $course_rs->getUserCourses(info => { username => 'lisa' }); + for my $user_course (@user_courses) { + removeIDs($user_course); + cleanUndef($user_course); + } + + my $courses = decode_json($ww3_dir->child('t/db/sample_data/courses.json')->slurp); + + my @user_courses_from_json; + for my $user (@$users) { + next unless $user->{username} eq 'lisa'; + for my $course_user_data (@{ $user->{courses} }) { + my $course = (grep { $_->{course_name} eq $course_user_data->{course_name} } @$courses)[0]; + push(@user_courses_from_json, { %{ $course_user_data->{course_user} }, %$course }); + $user_courses_from_json[-1]{course_user_params} //= {}; + delete $user_courses_from_json[-1]{course_settings}; + } + } + + # Make sure that the order of the courses is the same. + @user_courses_from_json = sort { $a->{course_name} cmp $b->{course_name} } @user_courses_from_json; + @user_courses = sort { $a->{course_name} cmp $b->{course_name} } @user_courses; + + is(\@user_courses, \@user_courses_from_json, 'getUserCourses: get all courses for a given user'); + + # Try to get a list of courses from a non-existent user. + is( + dies { $course_rs->getUserCourses(info => { username => 'non_existent_user' }); }, + check_isa('DB::Exception::UserNotFound'), + 'getUserCourses: try to get a list of courses for a non-existent user' + ); +}; done_testing; diff --git a/t/db/004_course_users.t b/t/db/004_course_users.t index 79e5bdb4..dafc5efb 100644 --- a/t/db/004_course_users.t +++ b/t/db/004_course_users.t @@ -2,379 +2,373 @@ # This tests the basic database CRUD functions of course users. -use warnings; -use strict; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +use Test2::V0; -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -use Test2::V0; -use YAML::XS qw/LoadFile/; +use Mojo::File qw/curfile/; use Clone qw/clone/; -use Mojo::JSON qw/false/; - -use DB::Schema; -use TestUtils qw/loadCSV removeIDs/; - -# Load the database -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -# $schema->storage->debug(1); # print out the SQL commands. - -my $user_rs = $schema->resultset('User'); - -# Get a list of users from the CSV file -my @students = loadCSV("$main::ww3_dir/t/db/sample_data/students.csv"); -for my $student (@students) { - $student->{is_admin} = false; - $student->{course_user_params} = $student->{params}; - delete $student->{params}; -} - -# Filter only precalc students -my @precalc_students = grep { $_->{course_name} eq 'Precalculus' } @students; -for my $student (@precalc_students) { - delete $student->{course_name}; -} -@precalc_students = sort { $a->{username} cmp $b->{username} } @precalc_students; - -# Fetch all precalc users from the database. -my @precalc_users_from_db = $user_rs->getCourseUsers(info => { course_name => 'Precalculus' }, merged => 1); - -@precalc_users_from_db = sort { $a->{username} cmp $b->{username} } @precalc_users_from_db; -removeIDs($_) for @precalc_users_from_db; - -is(\@precalc_users_from_db, \@precalc_students, 'getUsers: get users from a course'); - -# getUsers: Test that an unknown course results in an error. -is( - dies { $user_rs->getCourseUsers(info => { course_name => 'unknown_course' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'getUsers: undefined course_name' -); - -# getUsers: Test that an unknown course_id results in an error. -is( - dies { $user_rs->getCourseUsers(info => { course_id => -3 }); }, - check_isa('DB::Exception::CourseNotFound'), - 'getUsers: undefined course_id' -); - -# getUsers: Test that not passing either a course_id or course_name results in an error. -is( - dies { $user_rs->getCourseUsers(info => { my_course => 'Precalculus' }); }, - check_isa('DB::Exception::ParametersNeeded'), - 'getUsers: course_name or course_id not passed in' -); - -# Test getUser - -my $user = $user_rs->getCourseUser( - info => { - course_name => 'Precalculus', - username => $precalc_students[0]->{username} - }, - merged => 1 -); -removeIDs($user); - -is($user, $precalc_students[0], 'getCourseUser: get one merged user'); - -# getUser: Test that an unknown course results in an error -is( - dies { $user_rs->getCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'getCourseUser: undefined course' -); - -# getUser: Test that an unknown user results in an error -is( - dies { $user_rs->getCourseUser(info => { course_name => 'Precalculus', username => 'unknown_user' }); }, - check_isa('DB::Exception::UserNotFound'), - 'getCourseUser: undefined user' -); - -# getUser: Test that an existing user who is not in the course returns an error. -is( - dies { $user_rs->getCourseUser(info => { course_name => 'Arithmetic', username => 'marge' }); }, - check_isa('DB::Exception::UserNotInCourse'), - 'getCourseUser: get a user that is not in the course' -); - -# addUser: Add a user to a course -# Remove the following user if already defined in the course -my $quimby = $user_rs->find({ - 'username' => 'quimby', -}); -$quimby->delete if defined($quimby); - -my $user_params = { - username => 'quimby', - first_name => 'Joe', - last_name => 'Quimby', - email => 'mayor_joe@springfield.gov', - student_id => '12345', - is_admin => false -}; +use Mojo::JSON qw/decode_json false/; + +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use BuildDB qw/loadPermissions addCourses addUsers/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs/; + +my $ww3_dir = curfile->dirname->dirname->dirname; + +dbSubtest 'course users' => sub ($schema) { + # Add the neccessary sample data to the database. + loadPermissions($schema, $ww3_dir); + addCourses($schema, $ww3_dir); + addUsers($schema, $ww3_dir); + + my $user_rs = $schema->resultset('User'); + + # Get a list of users from the JSON file + my $users = decode_json($ww3_dir->child('t/db/sample_data/users.json')->slurp); + + my @precalc_students; + for my $user (@$users) { + my $courses = delete $user->{courses} // []; + for my $course_user_data (@$courses) { + # Filter only precalc students + next unless $course_user_data->{course_name} eq 'Precalculus'; + $user->{is_admin} //= false; + $user->{student_id} //= undef; + $user->{section} = $course_user_data->{course_user}{section} // undef; + $user->{recitation} = $course_user_data->{course_user}{recitation} // undef; + $user->{course_user_params} = $course_user_data->{course_user}{course_user_params} // {}; + $user->{role} = $course_user_data->{course_user}{role}; + push(@precalc_students, $user); + } + } + @precalc_students = sort { $a->{username} cmp $b->{username} } @precalc_students; + + # Fetch all precalc users from the database. + my @precalc_users_from_db = $user_rs->getCourseUsers(info => { course_name => 'Precalculus' }, merged => 1); + @precalc_users_from_db = sort { $a->{username} cmp $b->{username} } @precalc_users_from_db; + removeIDs($_) for @precalc_users_from_db; + + is(\@precalc_users_from_db, \@precalc_students, 'getUsers: get users from a course'); + + # getUsers: Test that an unknown course results in an error. + is( + dies { $user_rs->getCourseUsers(info => { course_name => 'unknown_course' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getUsers: undefined course_name' + ); -my $course_user_params = { - username => 'quimby', - role => 'student', - course_user_params => {}, - section => undef, - recitation => undef, -}; + # getUsers: Test that an unknown course_id results in an error. + is( + dies { $user_rs->getCourseUsers(info => { course_id => -3 }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getUsers: undefined course_id' + ); -$user_rs->addGlobalUser(params => $user_params); -$user = $user_rs->addCourseUser( - info => { course_name => 'Arithmetic', username => 'quimby' }, - params => $course_user_params -); -for my $key (qw/username course_name/) { - delete $course_user_params->{$key}; -} - -removeIDs($user); -delete $user_params->{course_name}; - -is($user, $course_user_params, 'addCourseUser: add a user to a course'); - -# Check that adding a user returns a merged user. -my $quimby_db = $user_rs->addCourseUser( - info => { course_name => 'Precalculus', username => 'quimby' }, - params => $course_user_params, - merged => 1 -); -removeIDs($quimby_db); - -my $quimby_params = clone($course_user_params); -for my $key (keys %$user_params) { - $quimby_params->{$key} = $user_params->{$key}; -} -is($quimby_db, $quimby_params, 'addCourseUser: check that an added user is returned merged'); - -# Checking that if the course exists, but the user is already a member an exception is thrown. -is( - dies { $user_rs->addCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); }, - check_isa('DB::Exception::CourseNotFound'), - "addCourseUser: the course doesn't exist" -); - -# updateUser:Check that the user updates. -my $updated_user = { params => { comment => 'Mayor Joe is the best!!' }, recitation => '2' }; - -is( - dies { $user_rs->addCourseUser(info => { course_name => 'Arithmetic', username => 'moe' }); }, - check_isa('DB::Exception::UserAlreadyInCourse'), - 'addCourseUser: the user is already a member' -); - -# try to add a non-existent user from a course: -is( - dies { $user_rs->addCourseUser(info => { course_name => 'Arithmetic', username => 'non_existent_user' }) }, - check_isa('DB::Exception::UserNotFound'), - 'addCourseUser: try to add a non-existent user to a course' -); - -# addCourseUser: add a user with undefined parameters -is( - dies { - $user_rs->addCourseUser( - info => { course_name => 'Topology', username => 'quimby', }, - params => { role => 'student', undefined_field => 1 } - ); - }, - check_isa('DBIx::Class::Exception'), - 'addCourseUser: an undefined field is passed in' -); - -# Add a user with undefined course user parameters. -is( - dies { - $user_rs->addCourseUser( - info => { course_name => 'Topology', username => 'quimby' }, - params => { role => 'student', course_user_params => { this_is_not_valid => 1 } } - ); - }, - check_isa('DB::Exception::InvalidField'), - 'addCourseUser: an undefined parameter is set' -); - -# Add a user with nonvalid fields -is( - dies { - $user_rs->addCourseUser( - info => { course_name => 'Topology', username => 'quimby' }, - params => { role => 'student', course_user_params => { useMathQuill => 0 } } - ); - }, - check_isa('DB::Exception::InvalidParameter'), - 'addCourseUser: an parameter with invalid value' -); - -# Add a user with an invalid role -is( - dies { - $user_rs->addCourseUser( - info => { course_name => 'Topology', username => 'quimby' }, - params => { role => 'cop' } - ); - }, - check_isa('DB::Exception::UserRoleUndefined'), - 'addCourseUser: try to add a user with an undefined user role' -); - -# updateCourseUser: check that the user updates. -$updated_user = { - course_user_params => { - comment => 'Mayor Joe is the best!!', - }, - recitation => '2' -}; + # getUsers: Test that not passing either a course_id or course_name results in an error. + is( + dies { $user_rs->getCourseUsers(info => { my_course => 'Precalculus' }); }, + check_isa('DB::Exception::ParametersNeeded'), + 'getUsers: course_name or course_id not passed in' + ); + + # Test getUser + + my $user = $user_rs->getCourseUser( + info => { + course_name => 'Precalculus', + username => $precalc_students[0]->{username} + }, + merged => 1 + ); + removeIDs($user); + + is($user, $precalc_students[0], 'getCourseUser: get one merged user'); + + # getUser: Test that an unknown course results in an error + is( + dies { $user_rs->getCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); } + , + check_isa('DB::Exception::CourseNotFound'), + 'getCourseUser: undefined course' + ); + + # getUser: Test that an unknown user results in an error + is( + dies { + $user_rs->getCourseUser(info => { course_name => 'Precalculus', username => 'unknown_user' }); + }, + check_isa('DB::Exception::UserNotFound'), + 'getCourseUser: undefined user' + ); + + # getUser: Test that an existing user who is not in the course returns an error. + is( + dies { $user_rs->getCourseUser(info => { course_name => 'Arithmetic', username => 'marge' }); }, + check_isa('DB::Exception::UserNotInCourse'), + 'getCourseUser: get a user that is not in the course' + ); + + # addUser: Add a user to a course + # Remove the following user if already defined in the course + my $quimby = $user_rs->find({ + 'username' => 'quimby', + }); + $quimby->delete if defined($quimby); + + my $user_params = { + username => 'quimby', + first_name => 'Joe', + last_name => 'Quimby', + email => 'mayor_joe@springfield.gov', + student_id => '12345', + is_admin => false + }; + + my $course_user_params = { + username => 'quimby', + role => 'student', + course_user_params => {}, + section => undef, + recitation => undef, + }; + + $user_rs->addGlobalUser(params => $user_params); + $user = $user_rs->addCourseUser( + info => { course_name => 'Arithmetic', username => 'quimby' }, + params => $course_user_params + ); + for my $key (qw/username course_name/) { + delete $course_user_params->{$key}; + } + + removeIDs($user); + delete $user_params->{course_name}; + + is($user, $course_user_params, 'addCourseUser: add a user to a course'); + + # Check that adding a user returns a merged user. + my $quimby_db = $user_rs->addCourseUser( + info => { course_name => 'Precalculus', username => 'quimby' }, + params => $course_user_params, + merged => 1 + ); + removeIDs($quimby_db); + + my $quimby_params = clone($course_user_params); + for my $key (keys %$user_params) { + $quimby_params->{$key} = $user_params->{$key}; + } + is($quimby_db, $quimby_params, 'addCourseUser: check that an added user is returned merged'); + + # Checking that if the course exists, but the user is already a member an exception is thrown. + is( + dies { $user_rs->addCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); } + , + check_isa('DB::Exception::CourseNotFound'), + q{addCourseUser: the course doesn't exist} + ); + + # updateUser:Check that the user updates. + my $updated_user = { params => { comment => 'Mayor Joe is the best!!' }, recitation => '2' }; + + is( + dies { $user_rs->addCourseUser(info => { course_name => 'Arithmetic', username => 'moe' }); }, + check_isa('DB::Exception::UserAlreadyInCourse'), + 'addCourseUser: the user is already a member' + ); + + # try to add a non-existent user from a course: + is( + dies { + $user_rs->addCourseUser(info => { course_name => 'Arithmetic', username => 'non_existent_user' }) + }, + check_isa('DB::Exception::UserNotFound'), + 'addCourseUser: try to add a non-existent user to a course' + ); + + # addCourseUser: add a user with undefined parameters + is( + dies { + $user_rs->addCourseUser( + info => { course_name => 'Topology', username => 'quimby', }, + params => { role => 'student', undefined_field => 1 } + ); + }, + check_isa('DBIx::Class::Exception'), + 'addCourseUser: an undefined field is passed in' + ); + + # Add a user with undefined course user parameters. + is( + dies { + $user_rs->addCourseUser( + info => { course_name => 'Topology', username => 'quimby' }, + params => { role => 'student', course_user_params => { this_is_not_valid => 1 } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'addCourseUser: an undefined parameter is set' + ); + + # Add a user with nonvalid fields + is( + dies { + $user_rs->addCourseUser( + info => { course_name => 'Topology', username => 'quimby' }, + params => { role => 'student', course_user_params => { useMathQuill => 0 } } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addCourseUser: an parameter with invalid value' + ); + + # Add a user with an invalid role + is( + dies { + $user_rs->addCourseUser( + info => { course_name => 'Topology', username => 'quimby' }, + params => { role => 'cop' } + ); + }, + check_isa('DB::Exception::UserRoleUndefined'), + 'addCourseUser: try to add a user with an undefined user role' + ); + + # updateCourseUser: check that the user updates. + $updated_user = { + course_user_params => { + comment => 'Mayor Joe is the best!!', + }, + recitation => '2' + }; + + for my $key (keys %$updated_user) { + $course_user_params->{$key} = $updated_user->{$key}; + } + + my $user_from_db = $user_rs->updateCourseUser( + info => { course_name => 'Arithmetic', username => 'quimby' }, + params => $updated_user + ); + + removeIDs($user_from_db); + is($user_from_db, $course_user_params, 'updateCourseUser: update a single user in an existing course.'); + + # updateCourseUser: check that if the course doesn't exist, an error is thrown: + is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'unknown_course', username => 'barney' }, + params => $updated_user + ); + }, + check_isa('DB::Exception::CourseNotFound'), + q{updateCourseUser: the course doesn't exist} + ); + + # updateCourseUser: check that if the course exists, but the user not a member. + is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'Arithmetic', username => 'marge' }, + params => $updated_user + ); + }, + check_isa('DB::Exception::UserNotInCourse'), + 'updateCourseUser: the user is not a member of the course' + ); + + # Try to add a non-existent user from a course. + is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'Arithmetic', user_name => 'bart' }, + params => $updated_user + ); + }, + check_isa('DB::Exception::ParametersNeeded'), + 'updateCourseUser: the incorrect information is passed in.' + ); + + # Check that a non-existent course throws an error. + is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'Arithmetic', username => 'quimby' }, + params => { sleeps_in_class => 1 } + ); + }, + check_isa('DBIx::Class::Exception'), + 'updateCourseUser: an invalid field is set' + ); + + # updateCourseUser: update a user with undefined parameters + is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'Arithmetic', username => 'quimby' }, + params => { course_user_params => { this_is_not_valid => 1 } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'updateCourseUser: an undefined parameter is set' + ); -for my $key (keys %$updated_user) { - $course_user_params->{$key} = $updated_user->{$key}; -} - -my $user_from_db = $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', username => 'quimby' }, - params => $updated_user -); - -removeIDs($user_from_db); -is($user_from_db, $course_user_params, 'updateCourseUser: update a single user in an existing course.'); - -# updateCourseUser: check that if the course doesn't exist, an error is thrown: -is( - dies { - $user_rs->updateCourseUser( - info => { course_name => 'unknown_course', username => 'barney' }, - params => $updated_user - ); - }, - check_isa('DB::Exception::CourseNotFound'), - "updateCourseUser: the course doesn't exist" -); - -# updateCourseUser: check that if the course exists, but the user not a member. -is( - dies { - $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', username => 'marge' }, - params => $updated_user - ); - }, - check_isa('DB::Exception::UserNotInCourse'), - 'updateCourseUser: the user is not a member of the course' -); - -# Try to add a non-existent user from a course. -is( - dies { - $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', user_name => 'bart' }, - params => $updated_user - ); - }, - check_isa('DB::Exception::ParametersNeeded'), - 'updateCourseUser: the incorrect information is passed in.' -); - -# Check that a non-existent course throws an error. -is( - dies { - $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', username => 'quimby' }, - params => { sleeps_in_class => 1 } - ); - }, - check_isa('DBIx::Class::Exception'), - 'updateCourseUser: an invalid field is set' -); - -# updateCourseUser: update a user with undefined parameters -is( - dies { - $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', username => 'quimby' }, - params => { course_user_params => { this_is_not_valid => 1 } } - ); - }, - check_isa('DB::Exception::InvalidField'), - 'updateCourseUser: an undefined parameter is set' -); - -# Check that updating a user with nonvalid fields throws an error. -is( - dies { - $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', username => 'quimby' }, - params => { course_user_params => { useMathQuill => 'yes' } } - ); - }, - check_isa('DB::Exception::InvalidParameter'), - 'updateCourseUser: an parameter with invalid value' -); - -# Delete a single user from a course. -my $deleted_user; -my $dont_delete_users; # Switch to not delete added users. - -SKIP: { - skip 'delete added users', 5 if $dont_delete_users; - - my $deleted_course_user = $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username => 'quimby' }); + # Check that updating a user with nonvalid fields throws an error. + is( + dies { + $user_rs->updateCourseUser( + info => { course_name => 'Arithmetic', username => 'quimby' }, + params => { course_user_params => { useMathQuill => 'yes' } } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'updateCourseUser: an parameter with invalid value' + ); + + # Delete a single user from a course. + my $deleted_course_user = + $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username => 'quimby' }); removeIDs($deleted_course_user); is($deleted_course_user, $course_user_params, 'deleteCourseUser: delete a user from a course'); - $deleted_user = $user_rs->deleteGlobalUser(info => { username => 'quimby' }); + my $deleted_user = $user_rs->deleteGlobalUser(info => { username => 'quimby' }); removeIDs($deleted_user); is($deleted_user, $user_params, 'deleteGlobalUser: delete a user'); # deleteUser: Check that if the course doesn't exist, an error is thrown: is( - dies { $user_rs->deleteCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); }, + dies { + $user_rs->deleteCourseUser(info => { course_name => 'unknown_course', username => 'barney' }); + }, check_isa('DB::Exception::CourseNotFound'), - "deleteUser: the course doesn't exist" + q{deleteUser: the course doesn't exist} ); # deleteUser: Check that if the course exists, but the user not a member. is( - dies { $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username => 'marge' }); }, + dies { + $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username => 'marge' }); + }, check_isa('DB::Exception::UserNotInCourse'), 'deleteUser: the user is not a member of the course' ); # deleteUser: Send in username_name instead of username is( - dies { $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username_name => 'bart' }); }, + dies { + $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username_name => 'bart' }); + }, check_isa('DB::Exception::ParametersNeeded'), 'deleteUser: the incorrect information is passed in.' ); -} - -# Check that the precalc users have not changed. -@precalc_users_from_db = $user_rs->getCourseUsers(info => { course_name => 'Precalculus' }, merged => 1); - -@precalc_users_from_db = sort { $a->{username} cmp $b->{username} } @precalc_users_from_db; -for (@precalc_users_from_db) { removeIDs($_); } - -is(\@precalc_users_from_db, \@precalc_students, 'check: ensure that the precalc users in the database is restored.'); +}; done_testing; diff --git a/t/db/005_hwsets.t b/t/db/005_hwsets.t index 357960d8..c492b334 100644 --- a/t/db/005_hwsets.t +++ b/t/db/005_hwsets.t @@ -2,436 +2,389 @@ # This tests the basic database CRUD functions of problem sets. -use warnings; -use strict; +use Test2::V0; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; +use Mojo::File qw/curfile/; +use Mojo::JSON qw/encode_json decode_json true false/; -use Test2::V0; -use Clone qw/clone/; -use YAML::XS qw/LoadFile/; -use Mojo::JSON qw/true false/; - -use DB::Schema; -use TestUtils qw/loadCSV removeIDs filterBySetType/; - -# Load the database -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -# $schema->storage->debug(1); # print out the SQL commands. - -my $problem_set_rs = $schema->resultset('ProblemSet'); -my $course_rs = $schema->resultset('Course'); -my $user_rs = $schema->resultset('User'); - -# Load HW sets from CSV file -my @hw_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/hw_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] - } -); -for my $hw_set (@hw_sets) { - $hw_set->{set_type} = 'HW'; - $hw_set->{set_params} = {} unless defined $hw_set->{set_params}; - -} - -my @quizzes = loadCSV( - "$main::ww3_dir/t/db/sample_data/quizzes.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => ['timed'], - param_non_neg_int_fields => ['quiz_duration'] - } -); -for my $quiz (@quizzes) { - $quiz->{set_type} = "QUIZ"; - $quiz->{set_params} = {} unless defined($quiz->{set_params}); -} - -my @review_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/review_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => ['can_retake'] +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use BuildDB qw/addCourses addSets/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs/; + +my $ww3_dir = curfile->dirname->dirname->dirname; + +dbSubtest 'problem sets' => sub ($schema) { + # Add the neccessary sample data to the database. + addCourses($schema, $ww3_dir); + addSets($schema, $ww3_dir); + + my $problem_set_rs = $schema->resultset('ProblemSet'); + + # Onle the precalculus sets are needed for this test. + my (@precalc_sets, @precalc_hw); + + # Load HW sets from JSON file. + my $course_hw_sets = decode_json($ww3_dir->child('t/db/sample_data/hw_sets.json')->slurp); + my @hw_sets; + for my $course_data (@$course_hw_sets) { + next unless $course_data->{course_name} eq 'Precalculus'; + for my $hw_set (@{ $course_data->{sets} }) { + $hw_set->{set_type} = 'HW'; + push(@precalc_sets, $hw_set); + push(@precalc_hw, $hw_set); + } } -); -for my $set (@review_sets) { - $set->{set_type} = 'REVIEW'; - $set->{set_params} = {} unless defined $set->{set_params}; - -} - -# Test getting all problem sets -my @all_problem_sets = (@hw_sets, @quizzes, @review_sets); - -# clone the sets since we need the original sets for the end of the test. -@all_problem_sets = @{ clone \@all_problem_sets }; - -my @problem_sets_from_db = $problem_set_rs->getAllProblemSets; - -@problem_sets_from_db = sort { $a->{set_name} cmp $b->{set_name} } @problem_sets_from_db; -@all_problem_sets = sort { $a->{set_name} cmp $b->{set_name} } @all_problem_sets; - -# Remove the id tags -for my $set (@problem_sets_from_db) { - removeIDs($set); - # Remove information about the course - delete $set->{visible}; - delete $set->{course_dates}; -} - -is(\@problem_sets_from_db, \@all_problem_sets, 'getProblemSets: get all sets'); - -# Filter the precalculus sets: -my @precalc_sets = filterBySetType(\@all_problem_sets, undef, 'Precalculus'); - -# Make a clone of the sets: -my $all_precalc_sets = clone(\@precalc_sets); - -for my $set (@$all_precalc_sets) { - delete $set->{course_name}; -} - -# Test for all sets in one course - -my @all_precalc_sets = sort { $a->{set_name} cmp $b->{set_name} } @$all_precalc_sets; - -my @precalc_sets_from_db = $problem_set_rs->getProblemSets(info => { course_name => 'Precalculus' }); - -# Remove id tags -for my $set (@precalc_sets_from_db) { - removeIDs($set); -} - -is(\@precalc_sets_from_db, \@all_precalc_sets, 'getProblemSets: get sets for one course'); - -# Test all HW sets in one course -my @precalc_hw = filterBySetType(\@all_problem_sets, 'HW', 'Precalculus'); -for my $set (@precalc_hw) { - delete $set->{course_name}; -} -@precalc_hw = sort { $a->{set_name} cmp $b->{set_name} } @precalc_hw; -my @precalc_hw_from_db = $problem_set_rs->getHWSets(info => { course_name => 'Precalculus' }); - -# Remove id tags -for my $set (@precalc_hw_from_db) { - removeIDs($set); -} -is(\@precalc_hw_from_db, \@precalc_hw, 'getHWSets: get all homework for one course'); - -# Get one Problem set -my $set_one = $precalc_hw[0]; -my $set_from_db = - $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => $set_one->{set_name} }); -removeIDs($set_from_db); -is($set_from_db, $set_one, 'getProblemSet: get one homework'); - -# Get a problem set that doesn't exist. -is( - dies { - $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'nonexistent_set' }); - }, - check_isa('DB::Exception::SetNotInCourse'), - 'getProblemSet: non-existent set name' -); - -# Try to get a problem set that is not in a given course -is( - dies { $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_id => 7 }); }, - check_isa('DB::Exception::SetNotInCourse'), - 'getProblemSet: find a set that is not in a course' -); - -# Add a new problem set -my $new_set_params = { - set_name => "HW #9", - set_dates => { - open => 100, - reduced_scoring => 120, - due => 140, - answer => 200, - enable_reduced_scoring => true - }, - set_params => {}, - set_type => "HW" -}; -my $new_set = $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set_params + # Load quiz sets from JSON file. + my $course_quizzes = decode_json($ww3_dir->child('t/db/sample_data/quizzes.json')->slurp); + my @quizzes; + for my $course_data (@$course_quizzes) { + next unless $course_data->{course_name} eq 'Precalculus'; + for my $quiz (@{ $course_data->{sets} }) { + $quiz->{set_type} = 'QUIZ'; + push(@precalc_sets, $quiz); + } } -); -my $new_set_id = $new_set->{set_id}; -removeIDs($new_set); -delete $new_set->{type}; -# add the default set_visible -$new_set_params->{set_visible} = false; -is($new_set, $new_set_params, "addProblemSet: add one homework"); - -# Try to add a homework without set_name -my $new_set2 = { - name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 200 }, - set_type => 'HW' -}; -is( - dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set2 }); }, - check_isa('DB::Exception::ParametersNeeded'), - 'addProblemSet: set_name not passed in.' -); - -# Try to add a homework with bad date fields -my $new_set3 = { - set_name => 'HW #11', - set_dates => { open_set => 100, due => 140, answer => 200 }, - set_type => 'HW' -}; -is( - dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set3 }); }, - check_isa('DB::Exception::InvalidField'), - 'addProblemSet: invalid date field passed in.' -); - -# Try to add a homework set without all required date fields -my $new_set4 = { - set_name => 'HW #11', - set_dates => { open => 100, due => 140 }, - set_type => 'HW' -}; -is( - dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set4 }); }, - check_isa('DB::Exception::FieldsNeeded'), - 'addProblemSet: missing required date fields' -); - -# Try to add a homework set without all required date fields -my $new_set5 = { - set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => '1234s' }, - set_type => 'HW' -}; -is( - dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set5 }); }, - check_isa('DB::Exception::InvalidParameter'), - 'addProblemSet: adding a non-numeric date' -); - -# Try to add a homework set without invalid date order -my $new_set6 = { - set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 10 }, - set_type => 'HW', - set_params => {} -}; -is( - dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set6 }); }, - check_isa('DB::Exception::ImproperDateOrder'), - 'addProblemSet: adding an illegal date order.' -); - -# Check for undefined parameter fields -my $new_set7 = { - set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, - set_type => 'HW', - set_params => { not_a_valid_field => 5 } -}; -is( - dies { $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set7 }); }, - check_isa('DB::Exception::InvalidField'), - 'addProblemSet: adding an undefined parameter field' -); - -# Check for invalid parameter fields (the hide_hint param is a boolean) -is( - dies { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, - set_type => 'HW', - set_params => { hide_hint => 'yes' } - } - ); - }, - check_isa('DB::Exception::InvalidParameter'), - 'addProblemSet: adding an non-valid parameter' -); - -# Check to ensure true/false are passed into the set_params, not 0/1 -is( - dies { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, - set_type => 'HW', - set_params => { hide_hint => 0 } - } - ); - }, - check_isa('DB::Exception::InvalidParameter'), - 'addProblemSet: adding an non-valid boolean parameter' -); - -# Check to ensure true/false are passed into the enable_reduced_scoring in set_dates, not 0/1 -is( - dies { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_name => 'HW #11', - set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => 0 }, - set_type => 'HW', - set_params => { hide_hint => 0 } - } - ); - }, - check_isa('DB::Exception::InvalidParameter'), - 'addProblemSet: adding an non-valid boolean parameter in set_dates' -); - -# Update a set -$new_set_params->{set_name} = "HW #8"; -$new_set_params->{set_params} = { hide_hint => true }; -$new_set_params->{type} = 1; - -my $updated_set = $problem_set_rs->updateProblemSet( - info => { - course_name => 'Precalculus', - set_id => $new_set_id - }, - params => { - set_name => $new_set_params->{set_name}, - set_params => { - hide_hint => true + + # Load review sets from JSON file. + my $course_review_sets = decode_json($ww3_dir->child('t/db/sample_data/review_sets.json')->slurp); + my @review_sets; + for my $course_data (@$course_review_sets) { + next unless $course_data->{course_name} eq 'Precalculus'; + for my $review_set (@{ $course_data->{sets} }) { + $review_set->{set_type} = 'REVIEW'; + push(@precalc_sets, $review_set); } } -); -removeIDs($updated_set); -delete $new_set_params->{type}; -is($updated_set, $new_set_params, 'updateSet: change the set parameters'); - -# Update the set where the set_type is sent, but the type is not: -$new_set_params->{set_name} = 'HW #88'; -$new_set_params->{set_type} = 'HW'; -$new_set_params->{set_visible} = true; -delete $new_set_params->{type}; -$updated_set = $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_id => $new_set_id }, - params => $new_set_params -); - -removeIDs($updated_set); -is($updated_set, $new_set_params, "updateSet: update a set with set_type defined."); - -# Change the type of a problem set from a Homework Set to a Quiz. - -my $set_with_new_type_params = clone($new_set_params); -$set_with_new_type_params->{set_dates} = { open => 0, answer => 0, due => 0 }; -$set_with_new_type_params->{set_params} = {}; -$set_with_new_type_params->{set_type} = 'QUIZ'; - -my $set_with_new_type = $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_id => $new_set_id }, - params => { set_type => 'QUIZ' } -); -removeIDs($set_with_new_type); - -is($set_with_new_type, $set_with_new_type_params, 'updateSet: change the type of the problem set'); - -# Try to update a set with an illegal field -is( - dies { - $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_id => $new_set_id }, - params => { bad_field => 0 } - ); - }, - check_isa('DBIx::Class::Exception'), - 'updateProblemSet: use a non-existing field' -); - -# Try to update a set with an illegal date field -is( - dies { - $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_id => $new_set_id }, - params => { set_dates => { bad_date => 99 } } - ); - }, - check_isa('DB::Exception::InvalidField'), - 'updateSet: invalid date field passed in.' -); - -# Try to update a set with an dates in a bad order -is( - dies { - $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_id => $new_set_id }, - params => { set_dates => { open => 999, answer => 100 } } - ); - }, - check_isa('DB::Exception::ImproperDateOrder'), - 'updateSet: adding an illegal date order.' -); - -# Delete a set -my $deleted_set = $problem_set_rs->deleteProblemSet(info => { course_name => 'Precalculus', set_name => 'HW #88' }); -removeIDs($deleted_set); -is($deleted_set, $set_with_new_type_params, 'deleteProblemSet: delete a set'); - -# Try deleting a set with invalid course_name -is( - dies { $problem_set_rs->deleteProblemSet(info => { course_name => 'Not a course', set_name => 'HW #1' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'deleteCourse: try to delete a set from a not existent course.' -); - -# Try deleting a set that does not exist -is( - dies { $problem_set_rs->deleteProblemSet(info => { course_name => 'Precalculus', set_name => 'HW #99' }); }, - check_isa('DB::Exception::SetNotInCourse'), - 'deleteCourse: try to delete a set that not exist.' -); - -# ensure that the problem_sets table in the database is restored. -@all_problem_sets = (@hw_sets, @quizzes, @review_sets); -@problem_sets_from_db = $problem_set_rs->getAllProblemSets; - -@all_problem_sets = sort { $a->{set_name} cmp $b->{set_name} } @all_problem_sets; -@problem_sets_from_db = sort { $a->{set_name} cmp $b->{set_name} } @problem_sets_from_db; - -# Remove the id tags -for my $set (@problem_sets_from_db) { - removeIDs($set); - # Remove information that is returned about the course. - delete $set->{visible}; - delete $set->{course_dates}; - # delete $set->{course_name}; -} - -is(\@problem_sets_from_db, \@all_problem_sets, 'check: ensure that the problem_sets table is restored.'); + + @precalc_sets = sort { $a->{set_name} cmp $b->{set_name} } @precalc_sets; + @precalc_hw = sort { $a->{set_name} cmp $b->{set_name} } @precalc_hw; + + # Get all sets in one course. + my @precalc_sets_from_db = $problem_set_rs->getProblemSets(info => { course_name => 'Precalculus' }); + removeIDs($_) for @precalc_sets_from_db; + is(\@precalc_sets_from_db, \@precalc_sets, 'getProblemSets: get sets for one course'); + + # Get all HW sets in one course. + my @precalc_hw_from_db = $problem_set_rs->getHWSets(info => { course_name => 'Precalculus' }); + removeIDs($_) for @precalc_hw_from_db; + is(\@precalc_hw_from_db, \@precalc_hw, 'getHWSets: get all homework for one course'); + + # Get one problem set. + my $set_from_db = + $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => $precalc_hw[0]{set_name} }); + removeIDs($set_from_db); + is($set_from_db, $precalc_hw[0], 'getProblemSet: get one homework'); + + # Get a problem set that doesn't exist. + is( + dies { + $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'nonexistent_set' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'getProblemSet: non-existent set name' + ); + + # Try to get a problem set that is not in a given course. + is( + dies { $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_id => 7 }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'getProblemSet: find a set that is not in a course' + ); + + # Add a new problem set. + my $new_set_params = { + set_name => 'HW #9', + set_dates => { + open => 100, + reduced_scoring => 120, + due => 140, + answer => 200, + enable_reduced_scoring => true + }, + set_params => {}, + set_type => 'HW' + }; + + my $new_set = $problem_set_rs->addProblemSet(params => { course_name => 'Precalculus', %$new_set_params }); + is( + $new_set, + hash { + field set_id => match qr/^\d*$/; + field course_id => match qr/^\d*$/; + field set_name => $new_set_params->{set_name}; + field set_dates => $new_set_params->{set_dates}; + field set_params => $new_set_params->{set_params}; + field set_type => $new_set_params->{set_type}; + field set_visible => false; + end; + }, + 'addProblemSet: add one homework' + ); + + # Try to add a homework without set_name + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + name => 'HW #11', + set_dates => { open => 100, due => 140, answer => 200 }, + set_type => 'HW' + } + ); + }, + check_isa('DB::Exception::ParametersNeeded'), + 'addProblemSet: set_name not passed in.' + ); + + # Try to add a homework with bad date fields. + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open_set => 100, due => 140, answer => 200 }, + set_type => 'HW' + } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'addProblemSet: invalid date field passed in.' + ); + + # Try to add a homework set without all required date fields. + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open => 100, due => 140 }, + set_type => 'HW' + } + ); + }, + check_isa('DB::Exception::FieldsNeeded'), + 'addProblemSet: missing required date fields' + ); + + # Try to add a homework set without all required date fields + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open => 100, due => 140, answer => '1234s' }, + set_type => 'HW' + } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addProblemSet: adding a non-numeric date' + ); + + # Try to add a homework set without invalid date order + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open => 100, due => 140, answer => 10 }, + set_type => 'HW', + set_params => {} + } + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'addProblemSet: adding an illegal date order.' + ); + + # Check for undefined parameter fields + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, + set_type => 'HW', + set_params => { not_a_valid_field => 5 } + } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'addProblemSet: adding an undefined parameter field' + ); + + # Check for invalid parameter fields (the hide_hint param is a boolean) + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, + set_type => 'HW', + set_params => { hide_hint => 'yes' } + } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addProblemSet: adding an non-valid parameter' + ); + + # Check to ensure true/false are passed into the set_params, not 0/1 + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => false }, + set_type => 'HW', + set_params => { hide_hint => 0 } + } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addProblemSet: adding an non-valid boolean parameter' + ); + + # Check to ensure true/false are passed into the enable_reduced_scoring in set_dates, not 0/1 + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_name => 'HW #11', + set_dates => { open => 100, due => 140, answer => 200, enable_reduced_scoring => 0 }, + set_type => 'HW', + set_params => { hide_hint => 0 } + } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addProblemSet: adding an non-valid boolean parameter in set_dates' + ); + + # Update a set + $new_set_params->{set_name} = 'HW #8'; + $new_set_params->{set_params} = { hide_hint => true }; + + my $updated_set = $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_id => $new_set->{set_id} }, + params => { set_name => $new_set_params->{set_name}, set_params => $new_set_params->{set_params} } + ); + is( + $updated_set, + hash { + field set_id => $new_set->{set_id}; + field course_id => match qr/^\d*$/; + field set_name => $new_set_params->{set_name}; + field set_dates => $new_set_params->{set_dates}; + field set_params => $new_set_params->{set_params}; + field set_type => $new_set_params->{set_type}; + field set_visible => false; + end; + }, + 'updateSet: change the set parameters' + ); + + # Update a set where the set_type is sent, but the type is not. + $new_set_params->{set_name} = 'HW #88'; + $new_set_params->{set_type} = 'HW'; + $new_set_params->{set_visible} = true; + $updated_set = $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_id => $new_set->{set_id} }, + params => $new_set_params + ); + + removeIDs($updated_set); + is($updated_set, $new_set_params, 'updateSet: update a set with set_type defined.'); + + # Change the type of a problem set from a Homework Set to a Quiz. + $new_set_params->{set_dates} = { open => 0, answer => 0, due => 0 }; + $new_set_params->{set_params} = {}; + $new_set_params->{set_type} = 'QUIZ'; + + my $set_with_new_type = $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_id => $new_set->{set_id} }, + params => { set_type => 'QUIZ' } + ); + removeIDs($set_with_new_type); + + is($set_with_new_type, $new_set_params, 'updateSet: change the type of the problem set'); + + # Try to update a set with an illegal field + is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_id => $new_set->{set_id} }, + params => { bad_field => 0 } + ); + }, + check_isa('DBIx::Class::Exception'), + 'updateProblemSet: use a non-existing field' + ); + + # Try to update a set with an illegal date field + is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_id => $new_set->{set_id} }, + params => { set_dates => { bad_date => 99 } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'updateSet: invalid date field passed in.' + ); + + # Try to update a set with an dates in a bad order + is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_id => $new_set->{set_id} }, + params => { set_dates => { open => 999, answer => 100 } } + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'updateSet: adding an illegal date order.' + ); + + # Delete a set + my $deleted_set = + $problem_set_rs->deleteProblemSet( + info => { course_name => 'Precalculus', set_name => $new_set_params->{set_name} }); + removeIDs($deleted_set); + is($deleted_set, $new_set_params, 'deleteProblemSet: delete a set'); + + # Try deleting a set with invalid course_name + is( + dies { + $problem_set_rs->deleteProblemSet(info => { course_name => 'Not a course', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteCourse: try to delete a set from a not existent course.' + ); + + # Try deleting a set that does not exist + is( + dies { + $problem_set_rs->deleteProblemSet(info => { course_name => 'Precalculus', set_name => 'HW #99' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'deleteCourse: try to delete a set that not exist.' + ); +}; done_testing; diff --git a/t/db/006_quizzes.t b/t/db/006_quizzes.t index f921eb03..623b987f 100644 --- a/t/db/006_quizzes.t +++ b/t/db/006_quizzes.t @@ -2,388 +2,365 @@ # This tests the basic database CRUD functions of problem sets of type quiz. -use warnings; -use strict; +use Test2::V0; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json true false/; -use Test2::V0; -use YAML::XS qw/LoadFile/; -use Clone qw/clone/; -use Mojo::JSON qw/true false/; - -use DB::Schema; -use TestUtils qw/loadCSV removeIDs filterBySetType/; - -# Load the database -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -# $schema->storage->debug(1); # print out the SQL commands. - -my $problem_set_rs = $schema->resultset('ProblemSet'); -my $course_rs = $schema->resultset('Course'); -my $user_rs = $schema->resultset('User'); - -my @all_problem_sets; - -my @quizzes = loadCSV( - "$main::ww3_dir/t/db/sample_data/quizzes.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => ['timed'] +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use BuildDB qw/addCourses addSets/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs/; + +my $ww3_dir = curfile->dirname->dirname->dirname; + +dbSubtest 'course users' => sub ($schema) { + # Add the neccessary sample data to the database. + addCourses($schema, $ww3_dir); + addSets($schema, $ww3_dir); + + my $problem_set_rs = $schema->resultset('ProblemSet'); + + # Load quiz sets from JSON file + my $course_quizzes = decode_json($ww3_dir->child('t/db/sample_data/quizzes.json')->slurp); + my @quizzes; + for my $course (@$course_quizzes) { + for my $quiz (@{ $course->{sets} }) { + $quiz->{set_type} = 'QUIZ'; + $quiz->{course_name} = $course->{course_name}; + push(@quizzes, $quiz); + } } -); -for my $quiz (@quizzes) { - $quiz->{set_type} = 'QUIZ'; -} - -# Remove 'Quiz #9' if it exists -my $quiz_to_delete = $problem_set_rs->find({ set_name => 'Quiz #9' }); -$quiz_to_delete->delete if defined($quiz_to_delete); - -# Test: Get all quizzes from one course -my @precalc_quizzes = filterBySetType(\@quizzes, 'QUIZ', 'Precalculus'); -for my $quiz (@precalc_quizzes) { - delete $quiz->{course_name}; -} -@precalc_quizzes = sort { $a->{set_name} cmp $b->{set_name} } @precalc_quizzes; -my @precalc_quizzes_from_db = $problem_set_rs->getQuizzes(info => { course_name => 'Precalculus' }); - -# Remove id tags -for my $quiz (@precalc_quizzes_from_db) { - removeIDs($quiz); -} -is(\@precalc_quizzes_from_db, \@precalc_quizzes, 'getQuizzes: get all quizzes for one course'); - -# Get a single quiz -my $quiz_from_db = $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'Quiz #1' }); -removeIDs($quiz_from_db); -my @quiz_from_csv = grep { $_->{set_name} eq 'Quiz #1' } @precalc_quizzes; -delete $quiz_from_csv[0]->{type}; -is($quiz_from_db, $quiz_from_csv[0], 'getQuiz: get one quiz from a single course'); - -# Try to get a quiz that doesn't exist in a course that does. -is( - dies { - $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'nonexisent quiz' }); - }, - check_isa('DB::Exception::SetNotInCourse'), - 'getQuiz: non-existent set name' -); - -# Try to get a quiz from a course that doesn't exist. -is( - dies { - $problem_set_rs->getProblemSet(info => { course_name => 'nonexistent course', set_name => 'Quiz #1' }); - }, - check_isa('DB::Exception::CourseNotFound'), - 'getQuiz: try to get a quiz from a non-existent course' -); - -# Add a new quiz -my $new_quiz_params = { - set_name => 'Quiz #9', - set_dates => { open => 100, due => 140, answer => 200 }, - set_params => {}, - set_type => 'QUIZ' -}; -my $new_quiz = $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_quiz_params + # Remove 'Quiz #9' if it exists + my $quiz_to_delete = $problem_set_rs->find({ set_name => 'Quiz #9' }); + $quiz_to_delete->delete if defined($quiz_to_delete); + + # Test getting all quizzes from one course + my @precalc_quizzes = grep { $_->{set_type} eq 'QUIZ' && $_->{course_name} eq 'Precalculus' } @quizzes; + for my $quiz (@precalc_quizzes) { + delete $quiz->{course_name}; } -); - -removeIDs($new_quiz); -# add the default set_visible field -$new_quiz_params->{set_visible} = false; -is($new_quiz, $new_quiz_params, "addQuiz: add a new quiz"); - -# Try to add a quiz to a non existent course. -is( - dies { - $problem_set_rs->addProblemSet(params => { course_name => 'nonexistent course', set_name => 'Quiz #1' }); - }, - check_isa('DB::Exception::CourseNotFound'), - 'addQuiz: try to add a quiz from a non-existent course' -); - -# Try to add a quiz with non-valid parameters. -is( - dies { - $problem_set_rs->addProblemSet(params => - { course_name => 'Precalculus', set_type => 'QUIZ', set_name => 'Quiz #99', nonexistent_field => 1, }) - }, - check_isa('DBIx::Class::Exception'), - 'addQuiz: try to add a quiz with a bad parameter' -); - -# Try to add a quiz without specifying the name. -is( - dies { - $problem_set_rs->addProblemSet( - params => { course_name => 'Precalculus', set_type => 'QUIZ', set_visible => true, }); - }, - check_isa('DB::Exception::ParametersNeeded'), - 'addQuiz: try to add a quiz with a bad field' -); - -# Try to add a quiz with an undefined parameter. -is( - dies { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_name => 'Quiz #99', - set_visible => true, - set_params => { param1 => 0 }, - set_dates => { open => 10, due => 100, answer => 200, } - } - ); - }, - check_isa('DB::Exception::InvalidField'), - 'addQuiz: try to add a quiz with a undefined parameter' -); - -# Try to add a quiz with a non-valid parameter. -is( - dies { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_name => 'Quiz #99', - set_visible => true, - set_params => { timed => 'yes' }, - set_dates => { open => 10, due => 100, answer => 200, } - } - ); - }, - check_isa('DB::Exception::InvalidParameter'), - 'addQuiz: try to add a quiz with a non-valid parameter' -); - -# Try to add a quiz with a missing required date fields. -is( - dies { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_name => 'Quiz #99', - set_dates => { open => 10, due => 100 } - } - ); - }, - check_isa('DB::Exception::FieldsNeeded'), - 'addQuiz: try to add a quiz with a missing required date fields' -); - -# Try to add a quiz with an undefined date field. -is( - dies { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_name => 'Quiz #99', - set_visible => 1, - set_dates => { open => 10, due => 100, answer => 200, reduced_scoring => 300 } - } - ); - }, - check_isa('DB::Exception::InvalidField'), - 'addQuiz: try to add a quiz with an undefined date field' -); - -# Try to add a quiz with dates that are out of order. -is( - dies { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - set_type => 'QUIZ', - set_name => 'Quiz #99', - set_visible => 1, - set_dates => { open => 10, due => 300, answer => 200, } - } - ); - }, - check_isa('DB::Exception::ImproperDateOrder'), - 'addQuiz: try to add a quiz with dates that are out of order' -); -# Update the visibility of the quiz -my $updated_params = { set_visible => 0 }; -my $updated_quiz = $problem_set_rs->updateProblemSet( - info => { - course_name => 'Precalculus', - set_name => 'Quiz #9' - }, - params => $updated_params -); - -$new_quiz->{set_visible} = false; -$new_quiz->{set_params} = {}; -removeIDs($updated_quiz); -is($updated_quiz, $new_quiz, 'updateQuiz: successfully update the quiz'); - -# Update the params of the quiz -$updated_params = { set_params => { timed => true } }; -$updated_quiz = $problem_set_rs->updateProblemSet( - info => { - course_name => 'Precalculus', - set_name => 'Quiz #9' - }, - params => $updated_params -); -removeIDs($updated_quiz); -$new_quiz->{set_params} = { timed => true }; -is($updated_quiz, $new_quiz, 'updateQuiz: successfully update the params of the quiz'); - -# Update the dates of the quiz -$updated_params = { - set_dates => { - open => 400, - due => 500, - answer => 600 + @precalc_quizzes = sort { $a->{set_name} cmp $b->{set_name} } @precalc_quizzes; + my @precalc_quizzes_from_db = $problem_set_rs->getQuizzes(info => { course_name => 'Precalculus' }); + + # Remove id tags + for my $quiz (@precalc_quizzes_from_db) { + removeIDs($quiz); } + is(\@precalc_quizzes_from_db, \@precalc_quizzes, 'getQuizzes: get all quizzes for one course'); + + # Get a single quiz + my $quiz_from_db = + $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'Quiz #1' }); + removeIDs($quiz_from_db); + my @quiz_from_json = grep { $_->{set_name} eq 'Quiz #1' } @precalc_quizzes; + delete $quiz_from_json[0]->{type}; + is($quiz_from_db, $quiz_from_json[0], 'getQuiz: get one quiz from a single course'); + + # Try to get a quiz that doesn't exist in a course that does. + is( + dies { + $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'nonexisent quiz' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'getQuiz: non-existent set name' + ); + + # Try to get a quiz from a course that doesn't exist. + is( + dies { + $problem_set_rs->getProblemSet(info => { course_name => 'nonexistent course', set_name => 'Quiz #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getQuiz: try to get a quiz from a non-existent course' + ); + + # Add a new quiz + my $new_quiz_params = { + set_name => 'Quiz #9', + set_dates => { open => 100, due => 140, answer => 200 }, + set_params => {}, + set_type => 'QUIZ' + }; + + my $new_quiz = $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + %$new_quiz_params + } + ); + + removeIDs($new_quiz); + # add the default set_visible field + $new_quiz_params->{set_visible} = false; + is($new_quiz, $new_quiz_params, "addQuiz: add a new quiz"); + + # Try to add a quiz to a non existent course. + is( + dies { + $problem_set_rs->addProblemSet( + params => { course_name => 'nonexistent course', set_name => 'Quiz #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'addQuiz: try to add a quiz from a non-existent course' + ); + + # Try to add a quiz with non-valid parameters. + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_type => 'QUIZ', + set_name => 'Quiz #99', + nonexistent_field => 1, + } + ) + }, + check_isa('DBIx::Class::Exception'), + 'addQuiz: try to add a quiz with a bad parameter' + ); + + # Try to add a quiz without specifying the name. + is( + dies { + $problem_set_rs->addProblemSet( + params => { course_name => 'Precalculus', set_type => 'QUIZ', set_visible => true, }); + }, + check_isa('DB::Exception::ParametersNeeded'), + 'addQuiz: try to add a quiz with a bad field' + ); + + # Try to add a quiz with an undefined parameter. + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_type => 'QUIZ', + set_name => 'Quiz #99', + set_visible => true, + set_params => { param1 => 0 }, + set_dates => { open => 10, due => 100, answer => 200, } + } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'addQuiz: try to add a quiz with a undefined parameter' + ); + + # Try to add a quiz with a non-valid parameter. + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_type => 'QUIZ', + set_name => 'Quiz #99', + set_visible => true, + set_params => { timed => 'yes' }, + set_dates => { open => 10, due => 100, answer => 200, } + } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addQuiz: try to add a quiz with a non-valid parameter' + ); + + # Try to add a quiz with a missing required date fields. + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_type => 'QUIZ', + set_name => 'Quiz #99', + set_dates => { open => 10, due => 100 } + } + ); + }, + check_isa('DB::Exception::FieldsNeeded'), + 'addQuiz: try to add a quiz with a missing required date fields' + ); + + # Try to add a quiz with an undefined date field. + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_type => 'QUIZ', + set_name => 'Quiz #99', + set_visible => 1, + set_dates => { open => 10, due => 100, answer => 200, reduced_scoring => 300 } + } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'addQuiz: try to add a quiz with an undefined date field' + ); + + # Try to add a quiz with dates that are out of order. + is( + dies { + $problem_set_rs->addProblemSet( + params => { + course_name => 'Precalculus', + set_type => 'QUIZ', + set_name => 'Quiz #99', + set_visible => 1, + set_dates => { open => 10, due => 300, answer => 200, } + } + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'addQuiz: try to add a quiz with dates that are out of order' + ); + # Update the visibility of the quiz + my $updated_params = { set_visible => 0 }; + my $updated_quiz = $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => $updated_params + ); + + $new_quiz->{set_visible} = false; + $new_quiz->{set_params} = {}; + removeIDs($updated_quiz); + is($updated_quiz, $new_quiz, 'updateQuiz: successfully update the quiz'); + + # Update the params of the quiz + $updated_params = { set_params => { timed => true } }; + $updated_quiz = $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => $updated_params + ); + removeIDs($updated_quiz); + $new_quiz->{set_params} = { timed => true }; + is($updated_quiz, $new_quiz, 'updateQuiz: successfully update the params of the quiz'); + + # Update the dates of the quiz + $updated_params = { set_dates => { open => 400, due => 500, answer => 600 } }; + $updated_quiz = $problem_set_rs->updateProblemSet( + info => { + course_name => 'Precalculus', + set_name => 'Quiz #9' + }, + params => $updated_params + ); + removeIDs($updated_quiz); + $new_quiz->{set_dates} = $updated_params->{set_dates}; + is($updated_quiz, $new_quiz, 'updateQuiz: successfully update the dates of the quiz'); + + # Try to update a non-existent field of the quiz. + is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => { nonexistent_field => 1 } + ); + }, + check_isa('DBIx::Class::Exception'), + 'updateQuiz: try to update a quiz with a non-valid field' + ); + + # Try to update a non-existent param of the quiz. + is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => { set_params => { show_hint => 1 } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'updateQuiz: try to update a quiz with an undefined parameter' + ); + + # Try to update a parameter with a bad value + is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => { set_params => { timed => 'yes' } } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'updateQuiz: try to update a quiz with a non-valid field' + ); + + # Try to update a quiz with an invalid date + is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => { set_dates => { reduced_scoring => 1000 } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'updateQuiz: try to update a quiz with a non-valid date' + ); + + # Try to update a quiz with a date out of order. + is( + dies { + $problem_set_rs->updateProblemSet( + info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, + params => { set_dates => { open => 50, due => 40 } } + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'updateQuiz: try to update a quiz with out of order dates' + ); + + # Try to delete from a non-existent course. + is( + dies { + $problem_set_rs->deleteProblemSet( + info => { course_name => 'Course does not exist', set_name => 'Quiz #9' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteQuiz: try to delete a quiz from a non-existent course' + ); + + # Try to delete from a non-existent course. + is( + dies { $problem_set_rs->deleteProblemSet(info => { course_id => 9999, set_name => 'Quiz #9' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteQuiz: try to delete a quiz from a non-existent course_id' + ); + + # Try to delete from a non-existent set in a course. + is( + dies { + $problem_set_rs->deleteProblemSet(info => { course_name => 'Precalculus', set_name => 'Quiz #999' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'deleteQuiz: try to delete a non-existent quiz' + ); + + # Try to delete from a non-existent set in a course. + is( + dies { + $problem_set_rs->deleteProblemSet(info => { course_name => 'Precalculus', set_id => 99999 }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'deleteQuiz: try to delete a non-existent quiz as set_id' + ); + + # Try to delete from a non-existent set in a course: + my $deleted_quiz = $problem_set_rs->deleteProblemSet( + info => { + course_name => 'Precalculus', + set_name => 'Quiz #9' + } + ); + removeIDs($deleted_quiz); + is($deleted_quiz, $new_quiz, 'delete Quiz: successfully delete a quiz'); }; -$updated_quiz = $problem_set_rs->updateProblemSet( - info => { - course_name => 'Precalculus', - set_name => 'Quiz #9' - }, - params => $updated_params -); -removeIDs($updated_quiz); -$new_quiz->{set_dates} = clone($updated_params->{set_dates}); -is($updated_quiz, $new_quiz, 'updateQuiz: successfully update the dates of the quiz'); - -# Try to update a non-existent field of the quiz. -is( - dies { - $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, - params => { nonexistent_field => 1 } - ); - }, - check_isa('DBIx::Class::Exception'), - 'updateQuiz: try to update a quiz with a non-valid field' -); - -# Try to update a non-existent param of the quiz. -is( - dies { - $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, - params => { set_params => { show_hint => 1 } } - ); - }, - check_isa('DB::Exception::InvalidField'), - 'updateQuiz: try to update a quiz with an undefined parameter' -); - -# Try to update a parameter with a bad value -is( - dies { - $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, - params => { set_params => { timed => 'yes' } } - ); - }, - check_isa('DB::Exception::InvalidParameter'), - 'updateQuiz: try to update a quiz with a non-valid field' -); - -# Try to update a quiz with an invalid date -is( - dies { - $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, - params => { set_dates => { reduced_scoring => 1000 } } - ); - }, - check_isa('DB::Exception::InvalidField'), - 'updateQuiz: try to update a quiz with a non-valid date' -); - -# Try to update a quiz with a date out of order. -is( - dies { - $problem_set_rs->updateProblemSet( - info => { course_name => 'Precalculus', set_name => 'Quiz #9' }, - params => { set_dates => { open => 50, due => 40 } } - ); - }, - check_isa('DB::Exception::ImproperDateOrder'), - 'updateQuiz: try to update a quiz with out of order dates' -); - -# Try to delete from a non-existent course. -is( - dies { - $problem_set_rs->deleteProblemSet( - info => { course_name => 'Course does not exist', set_name => 'Quiz #9' }); - }, - check_isa('DB::Exception::CourseNotFound'), - 'deleteQuiz: try to delete a quiz from a non-existent course' -); - -# Try to delete from a non-existent course. -is( - dies { $problem_set_rs->deleteProblemSet(info => { course_id => 9999, set_name => 'Quiz #9' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'deleteQuiz: try to delete a quiz from a non-existent course_id' -); - -# Try to delete from a non-existent set in a course. -is( - dies { $problem_set_rs->deleteProblemSet(info => { course_name => 'Precalculus', set_name => 'Quiz #999' }); }, - check_isa('DB::Exception::SetNotInCourse'), - 'deleteQuiz: try to delete a non-existent quiz' -); - -# Try to delete from a non-existent set in a course. -is( - dies { $problem_set_rs->deleteProblemSet(info => { course_name => 'Precalculus', set_id => 99999 }); }, - check_isa('DB::Exception::SetNotInCourse'), - 'deleteQuiz: try to delete a non-existent quiz as set_id' -); - -# Try to delete from a non-existent set in a course: -my $deleted_quiz = $problem_set_rs->deleteProblemSet( - info => { - course_name => 'Precalculus', - set_name => 'Quiz #9' - } -); -removeIDs($deleted_quiz); -is($deleted_quiz, $new_quiz, 'delete Quiz: successfully delete a quiz'); - -# Ensure that the quizzes in the database are restored. -@precalc_quizzes_from_db = $problem_set_rs->getQuizzes(info => { course_name => 'Precalculus' }); - -# Remove id tags -for my $quiz (@precalc_quizzes_from_db) { - removeIDs($quiz); -} -is(\@precalc_quizzes_from_db, \@precalc_quizzes, 'check: ensure that the quizzes have been restored.'); done_testing; diff --git a/t/db/007_user_set.t b/t/db/007_user_set.t index ea100fef..1dea3db6 100644 --- a/t/db/007_user_set.t +++ b/t/db/007_user_set.t @@ -2,815 +2,704 @@ # This tests the basic database CRUD functions of problem sets of type quiz. -use warnings; -use strict; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +use Test2::V0; -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -use Test2::V0; +use Mojo::File qw/curfile/; use Clone qw/clone/; - -use YAML::XS qw/LoadFile/; -use Mojo::JSON qw/true false/; - -use DB::Schema; -use TestUtils qw/loadCSV removeIDs cleanUndef/; - -# Load the configuration files -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); - -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -# $schema->storage->debug(1); # print out the SQL commands. - -my $user_set_rs = $schema->resultset('UserSet'); -my $course_rs = $schema->resultset('Course'); -my $course_user_rs = $schema->resultset('CourseUser'); -my $problem_set_rs = $schema->resultset('ProblemSet'); -my $course = $course_rs->find({ course_id => 1 }); - -# Load info from CSV files -my @hw_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/hw_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] - } -); -for my $hw_set (@hw_sets) { - $hw_set->{set_type} = "HW"; - $hw_set->{set_params} = {} unless defined $hw_set->{set_params}; -} - -my @quizzes = loadCSV( - "$main::ww3_dir/t/db/sample_data/quizzes.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => ['timed'], - param_non_neg_int_fields => ['quiz_duration'] - } -); -for my $set (@quizzes) { - $set->{set_type} = "QUIZ"; - $set->{set_params} = {} unless defined $set->{set_params}; -} - -my @review_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/review_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => ['can_retake'] +use Mojo::JSON qw/decode_json true false/; + +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use BuildDB qw/loadPermissions addCourses addUsers addSets addUserSets/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs cleanUndef/; + +my $ww3_dir = curfile->dirname->dirname->dirname; + +dbSubtest 'course users' => sub ($schema) { + # Add the neccessary sample data to the database. + loadPermissions($schema, $ww3_dir); + addCourses($schema, $ww3_dir); + addUsers($schema, $ww3_dir); + addSets($schema, $ww3_dir); + addUserSets($schema, $ww3_dir); + + my $user_set_rs = $schema->resultset('UserSet'); + my $course_rs = $schema->resultset('Course'); + my $problem_set_rs = $schema->resultset('ProblemSet'); + my $course = $course_rs->find({ course_id => 1 }); + + # Load info from JSON files + # Load HW sets from JSON file. + my $course_hw_sets = decode_json($ww3_dir->child('t/db/sample_data/hw_sets.json')->slurp); + my @hw_sets; + for my $course_data (@$course_hw_sets) { + for my $hw_set (@{ $course_data->{sets} }) { + $hw_set->{set_type} = 'HW'; + $hw_set->{course_name} = $course_data->{course_name}; + push(@hw_sets, $hw_set); + } } -); - -for my $set (@review_sets) { - $set->{set_type} = "REVIEW"; - $set->{set_params} = {} unless defined $set->{set_params}; -} -my @all_problem_sets = (@hw_sets, @quizzes, @review_sets); - -my @all_user_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/user_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] + # Load quiz sets from JSON file. + my $course_quizzes = decode_json($ww3_dir->child('t/db/sample_data/quizzes.json')->slurp); + my @quizzes; + for my $course_data (@$course_quizzes) { + for my $quiz (@{ $course_data->{sets} }) { + $quiz->{set_type} = 'QUIZ'; + $quiz->{course_name} = $course_data->{course_name}; + push(@quizzes, $quiz); + } } -); -for my $set (@all_user_sets) { - $set->{set_version} = 0 unless defined($set->{set_version}); - # find the problem set type - my $s = - (grep { $_->{course_name} eq $set->{course_name} && $_->{set_name} eq $set->{set_name} } @all_problem_sets)[0]; - $set->{set_params} = {} unless defined $set->{set_params}; - $set->{set_type} = $s->{set_type}; -} - -my @merged_user_sets = @{ clone(\@all_user_sets) }; - -# Merge the sets - -for my $user_set (@merged_user_sets) { - my $set = (grep { $_->{course_name} eq $user_set->{course_name} && $_->{set_name} eq $user_set->{set_name} } - @all_problem_sets)[0]; - - # override problem set dates with userset dates if exist - my $dates = clone($set->{set_dates}); - for my $d (keys %{ $user_set->{set_dates} }) { - $dates->{$d} = $user_set->{set_dates}->{$d}; + # Load review sets from JSON file. + my $course_review_sets = decode_json($ww3_dir->child('t/db/sample_data/review_sets.json')->slurp); + my @review_sets; + for my $course_data (@$course_review_sets) { + for my $review_set (@{ $course_data->{sets} }) { + $review_set->{set_type} = 'REVIEW'; + $review_set->{course_name} = $course_data->{course_name}; + push(@review_sets, $review_set); + } } - # Determine params and dates overrides - my $params = clone($set->{set_params}); - for my $key (keys %{ $user_set->{set_params} }) { - $params->{$key} = $user_set->{set_params}->{$key}; + my @all_problem_sets = (@hw_sets, @quizzes, @review_sets); + + my $course_set_users = decode_json($ww3_dir->child('t/db/sample_data/user_sets.json')->slurp); + my @all_user_sets; + for my $course_data (@$course_set_users) { + for my $set_data (@{ $course_data->{sets} }) { + for my $user_data (@{ $set_data->{users} }) { + $user_data->{user_set}{course_name} = $course_data->{course_name}; + $user_data->{user_set}{set_name} = $set_data->{set_name}; + $user_data->{user_set}{username} = $user_data->{username}; + $user_data->{user_set}{set_version} //= 0; + $user_data->{user_set}{set_dates} //= {}; + $user_data->{user_set}{set_params} //= {}; + $user_data->{user_set}{set_type} = ( + grep { + $_->{course_name} eq $course_data->{course_name} && $_->{set_name} eq $set_data->{set_name} + } @all_problem_sets + )[0]{set_type}; + push(@all_user_sets, $user_data->{user_set}); + } + } } - $user_set->{set_params} = $params; - $user_set->{set_dates} = $dates; - $user_set->{set_version} = 1 unless defined($user_set->{set_version}); - $user_set->{set_type} = $set->{set_type} unless defined($user_set->{set_type}); - $user_set->{set_visible} = $set->{set_visible} unless defined($user_set->{set_visible}); -} + my @merged_user_sets = @{ clone(\@all_user_sets) }; -# Get all user sets for a given user in a course. -my @all_user_sets_from_db = $user_set_rs->getAllUserSets(); - -for my $set (@all_user_sets_from_db) { - removeIDs($set); - for my $key (keys %{$set}) { - delete $set->{$key} unless defined $set->{$key}; - } -} + # Merge the sets + for my $user_set (@merged_user_sets) { + my $set = (grep { $_->{course_name} eq $user_set->{course_name} && $_->{set_name} eq $user_set->{set_name} } + @all_problem_sets)[0]; -# sort both arrays -@all_user_sets = sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @all_user_sets; -@all_user_sets_from_db = - sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @all_user_sets_from_db; + # override problem set dates with userset dates if exist + my $dates = clone($set->{set_dates}); + for my $d (keys %{ $user_set->{set_dates} }) { + $dates->{$d} = $user_set->{set_dates}{$d}; + } -is(\@all_user_sets_from_db, \@all_user_sets, 'getAllUserSets: get all user sets for all courses'); + # Determine params and dates overrides + my $params = clone($set->{set_params}); + for my $key (keys %{ $user_set->{set_params} }) { + $params->{$key} = $user_set->{set_params}{$key}; + } -my @merged_sets_from_db = $user_set_rs->getAllUserSets(merged => 1); + $user_set->{set_params} = $params; + $user_set->{set_dates} = $dates; + $user_set->{set_version} = 1 unless defined($user_set->{set_version}); + $user_set->{set_type} = $set->{set_type} unless defined($user_set->{set_type}); + $user_set->{set_visible} = $set->{set_visible} unless defined($user_set->{set_visible}); + } -for my $merged_set (@merged_sets_from_db) { - removeIDs($merged_set); -} + # FIXME: The getAllUserSets method should not exist and should not be tested. + # Get all user sets for all courses. + my @all_user_sets_from_db = $user_set_rs->getAllUserSets(); -# sort both arrays -@merged_sets_from_db = - sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @merged_sets_from_db; -@merged_user_sets = - sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @merged_user_sets; + for my $set (@all_user_sets_from_db) { + removeIDs($set); + cleanUndef($set); + } -is(\@merged_sets_from_db, \@merged_user_sets, 'getAllUserSets: get all merged sets for all courses'); + # sort both arrays + @all_user_sets = + sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @all_user_sets; + @all_user_sets_from_db = + sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @all_user_sets_from_db; -# Get all user set for a given user in a course. + is(\@all_user_sets_from_db, \@all_user_sets, 'getAllUserSets: get all user sets for all courses'); -my @user_sets_for_one_user = grep { $_->{course_name} eq 'Precalculus' && $_->{username} eq 'homer' } @all_user_sets; + my @merged_sets_from_db = $user_set_rs->getAllUserSets(merged => 1); -my @user_sets_from_db = $user_set_rs->getUserSetsForUser( - info => { - course_name => $course->course_name, - username => 'homer' + for my $merged_set (@merged_sets_from_db) { + removeIDs($merged_set); } -); - -for my $user_set (@user_sets_from_db) { - removeIDs($user_set); - delete $user_set->{set_visible} unless defined($user_set->{set_visible}); -} - -# sort both arrays -@user_sets_from_db = sort { $a->{set_name} cmp $b->{set_name} } @user_sets_from_db; -@user_sets_for_one_user = sort { $a->{set_name} cmp $b->{set_name} } @user_sets_for_one_user; -is(\@user_sets_from_db, \@user_sets_for_one_user, 'getUserSets: get all user sets for one user'); + # sort both arrays + @merged_sets_from_db = + sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @merged_sets_from_db; + @merged_user_sets = + sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @merged_user_sets; -# Get all merged sets for a given user in a course + is(\@merged_sets_from_db, \@merged_user_sets, 'getAllUserSets: get all merged sets for all courses'); -my @merged_sets_for_one_user = - grep { $_->{course_name} eq 'Precalculus' && $_->{username} eq 'homer' } @merged_user_sets; + # Get all user set for a given user in a course. -@merged_sets_from_db = $user_set_rs->getUserSetsForUser( - info => { - course_name => $course->course_name, - username => 'homer' - }, - merged => 1 -); + my @user_sets_for_one_user = + grep { $_->{course_name} eq 'Precalculus' && $_->{username} eq 'homer' } @all_user_sets; -for my $merged_set (@merged_sets_from_db) { - removeIDs($merged_set); -} - -is(\@merged_sets_from_db, \@merged_sets_for_one_user, 'getUserSets: get all merged sets for one user'); + my @user_sets_from_db = + $user_set_rs->getUserSetsForUser(info => { course_name => $course->course_name, username => 'homer' }); -# get all user sets for a given set in a course + for my $user_set (@user_sets_from_db) { + removeIDs($user_set); + delete $user_set->{set_visible} unless defined($user_set->{set_visible}); + } -my @user_sets_for_one_set = grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @all_user_sets; + # sort both arrays + @user_sets_from_db = sort { $a->{set_name} cmp $b->{set_name} } @user_sets_from_db; + @user_sets_for_one_user = sort { $a->{set_name} cmp $b->{set_name} } @user_sets_for_one_user; -@user_sets_from_db = $user_set_rs->getUserSetsForSet(info => { course_name => 'Precalculus', set_name => 'HW #1' }); + is(\@user_sets_from_db, \@user_sets_for_one_user, 'getUserSets: get all user sets for one user'); -for my $user_set (@user_sets_from_db) { - removeIDs($user_set); - delete $user_set->{set_visible} unless defined($user_set->{set_visible}); -} + # Get all merged sets for a given user in a course -is(\@user_sets_from_db, \@user_sets_for_one_set, 'getUserSets: get all user sets for a set in a course'); + my @merged_sets_for_one_user = + grep { $_->{course_name} eq 'Precalculus' && $_->{username} eq 'homer' } @merged_user_sets; -# get all merged user sets for a given set in a course + @merged_sets_from_db = $user_set_rs->getUserSetsForUser( + info => { course_name => $course->course_name, username => 'homer' }, + merged => 1 + ); -my @merged_sets_for_one_set = - grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @merged_user_sets; + for my $merged_set (@merged_sets_from_db) { + removeIDs($merged_set); + } -@merged_sets_from_db = $user_set_rs->getUserSetsForSet( - info => { course_name => 'Precalculus', set_name => 'HW #1' }, - merged => 1 -); + is(\@merged_sets_from_db, \@merged_sets_for_one_user, 'getUserSets: get all merged sets for one user'); -for my $user_set (@merged_sets_from_db) { - removeIDs($user_set); -} - -is(\@merged_sets_from_db, \@merged_sets_for_one_set, 'getUserSets: get all merged sets for a set in a course'); - -# Try to get a user set from a non-existing course. -is( - dies { - $user_set_rs->getUserSetsForUser(info => { course_name => 'non_existent_course', username => 'homer' }); - }, - check_isa('DB::Exception::CourseNotFound'), - 'getUserSets: attempt to get user sets from a nonexistent course' -); - -# Try to get a user set from a non-existing course. -is( - dies { - $user_set_rs->getUserSetsForUser(info => { course_name => 'Precalculus', username => 'non_existent_user' }); - }, - check_isa('DB::Exception::UserNotFound'), - 'getUserSets: attempt to get user sets from a nonexistent user' -); - -# Try to get a user set from a user not in the course. -is( - dies { $user_set_rs->getUserSetsForUser(info => { course_name => 'non_existent_course', username => 'bart' }); } - , - check_isa('DB::Exception::CourseNotFound'), - 'getUserSets: attempt to get user sets from user not in the course' -); - -# Get a single UserSet -my $info = { - username => 'homer', - course_name => 'Precalculus', - set_name => 'HW #1' -}; -my $user_set = $user_set_rs->getUserSet(info => $info); - -my $user_set_from_csv = clone( - ( - grep { - $_->{course_name} eq 'Precalculus' - && $_->{username} eq $info->{username} - && $_->{set_name} eq $info->{set_name} - } @all_user_sets - )[0] -); - -removeIDs($user_set); -delete $user_set->{set_visible} unless defined($user_set->{set_visible}); - -is($user_set, $user_set_from_csv, 'getUserSet: get a user set from a course'); - -# Get a merged UserSet - -my $merged_set_from_csv = clone( - ( - grep { - $_->{course_name} eq 'Precalculus' - && $_->{username} eq $info->{username} - && $_->{set_name} eq $info->{set_name} - } @merged_user_sets - )[0] -); - -my $merged_set = $user_set_rs->getUserSet(info => $info, merged => 1); -removeIDs($merged_set); - -is($merged_set, $merged_set_from_csv, 'getUserSet: get a merged set from a course'); - -# Try to get a user set from a non-existent course. -is( - dies { - $user_set_rs->getUserSet( - info => { course_name => 'non_existent_course', username => 'homer', set_name => 'HW #1' }); - }, - check_isa('DB::Exception::CourseNotFound'), - 'getUserSet: try to get a user set from a non-existent course' -); - -# Try to get a user set from a non-existent user. -is( - dies { - $user_set_rs->getUserSet( - info => { course_name => 'Precalculus', username => 'non_existent_user', set_name => 'HW #1' }); - }, - check_isa('DB::Exception::UserNotFound'), - 'getUserSet: try to get a user set from a non-existent user' -); - -# Try to get a user set from a user not in the course. -is( - dies { - $user_set_rs->getUserSet( - info => { course_name => 'Precalculus', username => 'marge', set_name => 'HW #1' }); - }, - check_isa('DB::Exception::UserNotInCourse'), - 'getUserSet: try to get a user set not in the course' -); - -# Try to get a user set from a non-existent set. -is( - dies { - $user_set_rs->getUserSet( - info => { course_name => 'Precalculus', username => 'homer', set_name => 'HW #999' }); - }, - check_isa('DB::Exception::SetNotInCourse'), - 'getUserSet: try to get a user set from a non-existent set' -); - -# Add a user set -my $new_info = { - username => 'otto', - course_name => 'Precalculus', - set_name => 'HW #1' -}; + # get all user sets for a given set in a course -my $new_user_set = $user_set_rs->addUserSet(params => $new_info); -removeIDs($new_user_set); -cleanUndef($new_user_set); + my @user_sets_for_one_set = + grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @all_user_sets; -# Set the other default parameters. -$new_info->{set_dates} = {}; -$new_info->{set_params} = {}; -$new_info->{set_version} = 0; -$new_info->{set_type} = 'HW'; + @user_sets_from_db = + $user_set_rs->getUserSetsForSet(info => { course_name => 'Precalculus', set_name => 'HW #1' }); -is($new_user_set, $new_info, 'addUserSet: add a new user set'); + for my $user_set (@user_sets_from_db) { + removeIDs($user_set); + delete $user_set->{set_visible} unless defined($user_set->{set_visible}); + } -my $hw_set1 = clone((grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @hw_sets)[0]); + is(\@user_sets_from_db, \@user_sets_for_one_set, 'getUserSets: get all user sets for a set in a course'); -$hw_set1->{username} = 'frink'; + # get all merged user sets for a given set in a course -my $new_merged_set = $user_set_rs->addUserSet( - params => { - course_name => 'Precalculus', - set_name => 'HW #1', - username => 'frink' - }, - merged => 1 -); -removeIDs($new_merged_set); -# add the default value for set_version -$hw_set1->{set_version} = 0; - -is($new_merged_set, $hw_set1, 'addUserSet: add a new user set and check that it is merged correctly'); - -# Add a user set with a empty set of dates. - -my $new_user_params2 = { - username => 'ralph', - course_name => 'Arithmetic', - set_name => 'HW #3', - set_dates => {} -}; + my @merged_sets_for_one_set = + grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @merged_user_sets; -my $new_user_set2 = $user_set_rs->addUserSet(params => $new_user_params2); -removeIDs($new_user_set2); -cleanUndef($new_user_set2); - -# add some fields to the params that are added when writing to the DB. -$new_user_params2->{set_type} = 'HW'; -$new_user_params2->{set_version} = 0; -$new_user_params2->{set_params} = {}; - -is($new_user_set2, $new_user_params2, 'addUserSet: add a new user set with empty dates.'); - -# Test that adding a field that is not in the database is ignored. -my $user_set_params3 = { - username => 'otto', - course_name => 'Precalculus', - set_name => 'HW #3', - bad_field => 1, - set_version => 0, - set_params => {}, - set_dates => {} -}; -my $user_set3 = $user_set_rs->addUserSet(params => $user_set_params3); -removeIDs($user_set3); -cleanUndef($user_set3); - -# The bad_field isn't returned -delete $user_set_params3->{bad_field}; - -# and need to explicitly set the type -$user_set_params3->{set_type} = 'HW'; - -is($user_set3, $user_set_params3, 'addUserSet: add a user set with a bad field'); - -# Try to add a user set to a course that doesn't exist. -is( - dies { - $user_set_rs->addUserSet( - params => { username => 'otto', course_name => 'non existent course', set_name => 'HW #1' }); - }, - check_isa('DB::Exception::CourseNotFound'), - 'addUserSet: try to add a user set to a non-existent course' -); - -# Try to add a user set for a set that does not exist in a course. -is( - dies { - $user_set_rs->addUserSet( - params => { username => 'otto', course_name => 'Precalculus', set_name => 'HW #99' }); - }, - check_isa('DB::Exception::SetNotInCourse'), - 'addUserSet: try to add a user set to a non-existent set' -); - -# Try to add a user set for a user that is not in a course. -is( - dies { - $user_set_rs->addUserSet( - params => { username => 'ralph', course_name => 'Abstract Algebra', set_name => 'HW #1' }); - }, - check_isa('DB::Exception::UserNotInCourse'), - 'addUserSet: try to add a user set for a user who is not in the course' -); - -# Try to add a user_set that already exists. -is( - dies { - $user_set_rs->addUserSet( - params => { username => 'otto', course_name => 'Precalculus', set_name => 'HW #1' }); - }, - check_isa('DB::Exception::UserSetExists'), - 'addUserSet: try to add a user set that already exists' -); - -my $otto_set_info2 = { - username => 'otto', - course_name => 'Precalculus', - set_name => 'HW #2', - set_version => 1 -}; + @merged_sets_from_db = $user_set_rs->getUserSetsForSet( + info => { course_name => 'Precalculus', set_name => 'HW #1' }, + merged => 1 + ); -# Add a user set with valid params. -my $user_set2 = $user_set_rs->addUserSet( - params => { - %$otto_set_info2, - set_params => { - description => 'This is the description for HW #2' - } + for my $user_set (@merged_sets_from_db) { + removeIDs($user_set); } -); -removeIDs($user_set2); -cleanUndef($user_set2); - -my $set_params2 = clone($otto_set_info2); -# set the other default parameters: -$set_params2->{set_dates} = {}; -$set_params2->{set_params} = { description => 'This is the description for HW #2' }; -$set_params2->{set_version} = 1; -$set_params2->{set_type} = 'HW'; - -is($user_set2, $set_params2, 'addUserSet: add a new user set with params'); - -# When adding a user set with a bad field in the params, it is ignored -is( - dies { - $user_set_rs->addUserSet( - params => { - username => 'otto', - course_name => 'Precalculus', - set_name => 'HW #4', - set_version => 1, - set_params => { bad_field => 12, hide_hint => false } - } - ) - }, - check_isa('DB::Exception::InvalidField'), - 'addUserSet: try to add a new user set with an undefined parameter' -); - -# add a user set with a new date - -my $set_dates4 = { - open => 1, - reduced_scoring => 800, - due => 900, - answer => 1000 -}; - -my $otto_set_info4 = { - username => 'otto', - course_name => 'Precalculus', - set_name => 'HW #5', - set_version => 1 -}; -my $otto_set_info5 = { - username => 'otto', - course_name => 'Precalculus', - set_name => 'HW #6', -}; + is(\@merged_sets_from_db, \@merged_sets_for_one_set, 'getUserSets: get all merged sets for a set in a course'); + + # Try to get a user set from a non-existing course. + is( + dies { + $user_set_rs->getUserSetsForUser(info => { course_name => 'non_existent_course', username => 'homer' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getUserSets: attempt to get user sets from a nonexistent course' + ); + + # Try to get a user set from a non-existing course. + is( + dies { + $user_set_rs->getUserSetsForUser( + info => { course_name => 'Precalculus', username => 'non_existent_user' }); + }, + check_isa('DB::Exception::UserNotFound'), + 'getUserSets: attempt to get user sets from a nonexistent user' + ); + + # Try to get a user set from a user not in the course. + is( + dies { + $user_set_rs->getUserSetsForUser(info => { course_name => 'non_existent_course', username => 'bart' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getUserSets: attempt to get user sets from user not in the course' + ); + + # Get a single UserSet + my $info = { username => 'homer', course_name => 'Precalculus', set_name => 'HW #1' }; + my $user_set = $user_set_rs->getUserSet(info => $info); + + my $user_set_from_json = clone( + ( + grep { + $_->{course_name} eq 'Precalculus' + && $_->{username} eq $info->{username} + && $_->{set_name} eq $info->{set_name} + } @all_user_sets + )[0] + ); -my $otto_set_params4 = { %$otto_set_info4, set_dates => $set_dates4 }; + removeIDs($user_set); + delete $user_set->{set_visible} unless defined($user_set->{set_visible}); -my $user_set4 = $user_set_rs->addUserSet(params => $otto_set_params4); -removeIDs($user_set4); -cleanUndef($user_set4); + is($user_set, $user_set_from_json, 'getUserSet: get a user set from a course'); -my $set_params4 = clone($otto_set_params4); -$set_params4->{set_type} = 'HW'; -$set_params4->{set_params} = {}; -$set_params4->{set_version} = 1; + # Get a merged UserSet -is($user_set4, $set_params4, 'addUserSet: add a new user set with dates'); + my $merged_set_from_json = clone( + ( + grep { + $_->{course_name} eq 'Precalculus' + && $_->{username} eq $info->{username} + && $_->{set_name} eq $info->{set_name} + } @merged_user_sets + )[0] + ); -# add a user with only override dates set. + my $merged_set = $user_set_rs->getUserSet(info => $info, merged => 1); + removeIDs($merged_set); -my $hw4 = $problem_set_rs->getProblemSet( - info => { + is($merged_set, $merged_set_from_json, 'getUserSet: get a merged set from a course'); + + # Try to get a user set from a non-existent course. + is( + dies { + $user_set_rs->getUserSet( + info => { course_name => 'non_existent_course', username => 'homer', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getUserSet: try to get a user set from a non-existent course' + ); + + # Try to get a user set from a non-existent user. + is( + dies { + $user_set_rs->getUserSet( + info => { course_name => 'Precalculus', username => 'non_existent_user', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::UserNotFound'), + 'getUserSet: try to get a user set from a non-existent user' + ); + + # Try to get a user set from a user not in the course. + is( + dies { + $user_set_rs->getUserSet( + info => { course_name => 'Precalculus', username => 'marge', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::UserNotInCourse'), + 'getUserSet: try to get a user set not in the course' + ); + + # Try to get a user set from a non-existent set. + is( + dies { + $user_set_rs->getUserSet( + info => { course_name => 'Precalculus', username => 'homer', set_name => 'HW #999' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'getUserSet: try to get a user set from a non-existent set' + ); + + # Add a user set + my $new_info = { username => 'otto', course_name => 'Precalculus', set_name => 'HW #1' }; + + my $new_user_set = $user_set_rs->addUserSet(params => $new_info); + removeIDs($new_user_set); + cleanUndef($new_user_set); + + # Set the other default parameters. + $new_info->{set_dates} = {}; + $new_info->{set_params} = {}; + $new_info->{set_version} = 0; + $new_info->{set_type} = 'HW'; + + is($new_user_set, $new_info, 'addUserSet: add a new user set'); + + my $hw_set1 = + clone((grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @hw_sets)[0]); + + $hw_set1->{username} = 'frink'; + + my $new_merged_set = $user_set_rs->addUserSet( + params => { course_name => 'Precalculus', set_name => 'HW #1', username => 'frink' }, + merged => 1 + ); + removeIDs($new_merged_set); + # add the default value for set_version + $hw_set1->{set_version} = 0; + + is($new_merged_set, $hw_set1, 'addUserSet: add a new user set and check that it is merged correctly'); + + # Add a user set with a empty set of dates. + + my $new_user_params2 = + { username => 'ralph', course_name => 'Arithmetic', set_name => 'HW #3', set_dates => {} }; + + my $new_user_set2 = $user_set_rs->addUserSet(params => $new_user_params2); + removeIDs($new_user_set2); + cleanUndef($new_user_set2); + + # add some fields to the params that are added when writing to the DB. + $new_user_params2->{set_type} = 'HW'; + $new_user_params2->{set_version} = 0; + $new_user_params2->{set_params} = {}; + + is($new_user_set2, $new_user_params2, 'addUserSet: add a new user set with empty dates.'); + + # Test that adding a field that is not in the database is ignored. + my $user_set_params3 = { + username => 'otto', course_name => 'Precalculus', - set_name => 'HW #4' - } -); - -my $ralph_set_info = { - username => 'ralph', - course_name => 'Precalculus', - set_name => 'HW #4', - set_dates => { - open => $hw4->{set_dates}->{open} - 100, - answer => $hw4->{set_dates}->{answer} + 100, - enable_reduced_scoring => false, - }, - set_params => { - hide_hint => false - } -}; - -my $ralph_user_set = $user_set_rs->addUserSet(params => $ralph_set_info); -removeIDs($ralph_user_set); -cleanUndef($ralph_user_set); - -# set some fields that are created from defaults when written to the DB. -$ralph_set_info->{set_type} = 'HW'; -$ralph_set_info->{set_version} = 0; - -is($ralph_user_set, $ralph_set_info, 'addUserSet: add a new user with dates (some are missing).'); - -# Try to add a bad date. -is( - dies { - $user_set_rs->addUserSet( - params => { - %$otto_set_info5, - set_dates => { open => 100, due => 9, answer => 1000, enable_reduced_scoring => false } - } - ); - }, - check_isa('DB::Exception::ImproperDateOrder'), - 'addUserSet: dates are out of order' -); - -is( - dies { - $user_set_rs->addUserSet( - params => { %$otto_set_info5, set_dates => { open => 100, due => 900, answer => 800 } }); - }, - check_isa('DB::Exception::ImproperDateOrder'), - 'addUserSet: dates are out of order' -); -# Add a new user set and test that it is merged correctly. - -my $otto_quiz_info = { - course_name => 'Precalculus', - set_name => 'Quiz #1', - username => 'otto' -}; + set_name => 'HW #3', + bad_field => 1, + set_version => 0, + set_params => {}, + set_dates => {} + }; + my $user_set3 = $user_set_rs->addUserSet(params => $user_set_params3); + removeIDs($user_set3); + cleanUndef($user_set3); + + # The bad_field isn't returned + delete $user_set_params3->{bad_field}; + + # and need to explicitly set the type + $user_set_params3->{set_type} = 'HW'; + + is($user_set3, $user_set_params3, 'addUserSet: add a user set with a bad field'); + + # Try to add a user set to a course that doesn't exist. + is( + dies { + $user_set_rs->addUserSet( + params => { username => 'otto', course_name => 'non existent course', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'addUserSet: try to add a user set to a non-existent course' + ); + + # Try to add a user set for a set that does not exist in a course. + is( + dies { + $user_set_rs->addUserSet( + params => { username => 'otto', course_name => 'Precalculus', set_name => 'HW #99' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'addUserSet: try to add a user set to a non-existent set' + ); + + # Try to add a user set for a user that is not in a course. + is( + dies { + $user_set_rs->addUserSet( + params => { username => 'ralph', course_name => 'Abstract Algebra', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::UserNotInCourse'), + 'addUserSet: try to add a user set for a user who is not in the course' + ); + + # Try to add a user_set that already exists. + is( + dies { + $user_set_rs->addUserSet( + params => { username => 'otto', course_name => 'Precalculus', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::UserSetExists'), + 'addUserSet: try to add a user set that already exists' + ); + + my $otto_set_info2 = + { username => 'otto', course_name => 'Precalculus', set_name => 'HW #2', set_version => 1 }; + + # Add a user set with valid params. + my $user_set2 = $user_set_rs->addUserSet( + params => { + %$otto_set_info2, set_params => { description => 'This is the description for HW #2' } + } + ); + removeIDs($user_set2); + cleanUndef($user_set2); -# Then add a new user set and test that it is merged correctly. + my $set_params2 = clone($otto_set_info2); + # set the other default parameters: + $set_params2->{set_dates} = {}; + $set_params2->{set_params} = { description => 'This is the description for HW #2' }; + $set_params2->{set_version} = 1; + $set_params2->{set_type} = 'HW'; -my $merged_set1 = clone( - ( - grep { - $_->{course_name} eq $otto_quiz_info->{course_name} - && $_->{set_name} eq $otto_quiz_info->{set_name} - } @all_problem_sets - )[0] -); + is($user_set2, $set_params2, 'addUserSet: add a new user set with params'); -$merged_set1->{username} = $otto_quiz_info->{username}; + # When adding a user set with a bad field in the params, it is ignored + is( + dies { + $user_set_rs->addUserSet( + params => { + username => 'otto', + course_name => 'Precalculus', + set_name => 'HW #4', + set_version => 1, + set_params => { bad_field => 12, hide_hint => false } + } + ) + }, + check_isa('DB::Exception::InvalidField'), + 'addUserSet: try to add a new user set with an undefined parameter' + ); -my $new_dates = { - due => $merged_set->{set_dates}->{answer} + 1000, - answer => $merged_set->{set_dates}->{answer} + 2000 -}; + # add a user set with a new date -$merged_set1->{set_dates}->{due} = $new_dates->{due}; -$merged_set1->{set_dates}->{answer} = $new_dates->{answer}; - -my $user_set_to_merge = $user_set_rs->addUserSet( - params => { - %$otto_quiz_info, set_dates => $new_dates - }, - merged => 1 -); -removeIDs($user_set_to_merge); -# otto_quiz has set_version 0. Need to match to compare. -$merged_set1->{set_version} = 0; - -is($user_set_to_merge, $merged_set1, 'addUserSet: adding a user set with dates to check merging'); - -# Check that adding a user set that is out of order with the problem sets throws an error. -# This set has a due date after the answer date. -is( - dies { - $user_set_rs->addUserSet(params => { %$otto_set_info5, set_dates => { due => 1609595640 } }, merged => 1); - }, - check_isa('DB::Exception::ImproperDateOrder'), - 'addUserSet: user set is out of order with respect to problem set' -); - -# Check that setting a boolean as 0/1 throws an error -# Currently, this is stripped out if hide_hint is 0. Do we want to check for this? -is( - dies { - $user_set_rs->addUserSet(params => { %$otto_set_info5, set_params => { hide_hint => 1 } }, merged => 1); - }, - check_isa('DB::Exception::InvalidParameter'), - 'addUserSet: boolean valid should be JSON boolean' -); - -# Update User Set - -my $otto_quiz = $user_set_rs->getUserSet(info => $otto_quiz_info); -removeIDs($otto_quiz); - -# Update the dates -my $updated_dates = { - due => $otto_quiz->{set_dates}->{due} + 100, - answer => $otto_quiz->{set_dates}->{due} + 1000 -}; + my $set_dates4 = { open => 1, reduced_scoring => 800, due => 900, answer => 1000 }; -$otto_quiz->{set_dates}->{due} = $updated_dates->{due}; -$otto_quiz->{set_dates}->{answer} = $updated_dates->{answer}; + my $otto_set_info4 = + { username => 'otto', course_name => 'Precalculus', set_name => 'HW #5', set_version => 1 }; -my $updated_user_quiz = $user_set_rs->updateUserSet(info => $otto_quiz_info, params => $otto_quiz); -removeIDs($updated_user_quiz); + my $otto_set_info5 = { username => 'otto', course_name => 'Precalculus', set_name => 'HW #6' }; -is($updated_user_quiz, $otto_quiz, 'updateUserSet: update the dates'); + my $otto_set_params4 = { %$otto_set_info4, set_dates => $set_dates4 }; -# Update the params -my $updated_user_set2 = $user_set_rs->updateUserSet( - info => $otto_quiz_info, - params => { - set_params => { - problem_randorder => true, - } - } -); -removeIDs($updated_user_set2); -$otto_quiz->{set_params}->{problem_randorder} = true; -is($updated_user_set2, $otto_quiz, 'updateUserSet: update the params'); - -# Update a valid field -my $updated_user_set3 = $user_set_rs->updateUserSet( - info => $otto_quiz_info, - params => { - set_visible => true - } -); -removeIDs($updated_user_set3); -$otto_quiz->{set_visible} = true; - -is($otto_quiz, $updated_user_set3, 'updateUserSet: update the set visibility'); - -# Try updating an invalid param. -is( - dies { - $user_set_rs->updateUserSet( - info => $otto_quiz_info, - params => { set_params => { not_a_valid_param => 'bad' } } - ); - }, - check_isa('DB::Exception::InvalidField'), - 'updateUserSet: try setting a parameter that does not exist' -); - -# Try updating an invalid date. -is( - dies { - $user_set_rs->updateUserSet(info => $otto_quiz_info, params => { set_dates => { open => 1, closed => 2 } }); - }, - check_isa('DB::Exception::InvalidField'), - 'updateUserSet: try to update an invalid date field' -); - -# Test with out of order dates. -is( - dies { - $user_set_rs->updateUserSet( - info => $otto_quiz_info, - params => { set_dates => { open => 100, due => 2, answer => 200 } } - ); - }, - check_isa('DB::Exception::ImproperDateOrder'), - 'updateUserSet: try to update with out of order dates' -); - -# Try to update a user_set that doesn't exist. -is( - dies { - $user_set_rs->updateUserSet(info => $otto_set_info5, params => { set_params => { hide_hint => true } }); - }, - check_isa('DB::Exception::UserSetNotInCourse'), - 'updateUserSet: try to update a user set not the in the course' -); - -# Try to delete a user_set that doesn't exist. -is( - dies { $user_set_rs->deleteUserSet(info => $otto_set_info5); }, - check_isa('DB::Exception::UserSetNotInCourse'), - 'deleteUserSet: try to delete a user set not the in the course' -); - -# Delete some user sets that were created. - -my $deleted_user_set = $user_set_rs->deleteUserSet( - info => { - username => $new_user_set2->{username}, - course_name => $new_user_set2->{course_name}, - set_name => $new_user_set2->{set_name} - } -); -removeIDs($deleted_user_set); -cleanUndef($deleted_user_set); -removeIDs($new_user_set2); -cleanUndef($new_user_set2); -is($deleted_user_set, $new_user_set2, "deleteUserSet: successfully delete a user set"); - -my $deleted_user_set2 = $user_set_rs->deleteUserSet( - info => { - username => $ralph_user_set->{username}, - course_name => $ralph_user_set->{course_name}, - set_name => $ralph_user_set->{set_name} - } -); -removeIDs($deleted_user_set2); -cleanUndef($deleted_user_set2); -is($deleted_user_set2, $ralph_user_set, "deleteUserSet: successfully delete another user set"); - -my $deleted_user_set3 = $user_set_rs->deleteUserSet(info => $otto_quiz_info); -removeIDs($deleted_user_set3); -cleanUndef($deleted_user_set3); -is($deleted_user_set3, $otto_quiz, "deleteUserSet: successfully delete yet another user set"); - -my $deleted_user_set4 = $user_set_rs->deleteUserSet(info => $new_merged_set); - -# remove the rest of the user sets added for user 'otto' - -# Test that dates are merged correctly. -# First remove the sets added for user otto. - -my @otto_user_sets = $user_set_rs->search( - { - 'courses.course_name' => 'Precalculus', - 'users.username' => 'otto', - }, - { - join => [ { problem_set => 'courses' }, { course_users => 'users' } ] - } -); + my $user_set4 = $user_set_rs->addUserSet(params => $otto_set_params4); + removeIDs($user_set4); + cleanUndef($user_set4); -for my $u (@otto_user_sets) { - $u->delete; -} + my $set_params4 = clone($otto_set_params4); + $set_params4->{set_type} = 'HW'; + $set_params4->{set_params} = {}; + $set_params4->{set_version} = 1; -# Check that the user_sets db table is restored. -@all_user_sets_from_db = $user_set_rs->getAllUserSets(); + is($user_set4, $set_params4, 'addUserSet: add a new user set with dates'); -for my $set (@all_user_sets_from_db) { - removeIDs($set); - cleanUndef($set); - for my $key (keys %{$set}) { - delete $set->{$key} unless defined $set->{$key}; - } -} + # add a user with only override dates set. -# Sort before comparing. -@all_user_sets = sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @all_user_sets; -@all_user_sets_from_db = - sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @all_user_sets_from_db; + my $hw4 = $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'HW #4' }); -is(\@all_user_sets_from_db, \@all_user_sets, 'check: ensure that the user sets are restored.'); + my $ralph_set_info = { + username => 'ralph', + course_name => 'Precalculus', + set_name => 'HW #4', + set_dates => { + open => $hw4->{set_dates}{open} - 100, + answer => $hw4->{set_dates}{answer} + 100, + enable_reduced_scoring => false, + }, + set_params => { hide_hint => false } + }; + + my $ralph_user_set = $user_set_rs->addUserSet(params => $ralph_set_info); + removeIDs($ralph_user_set); + cleanUndef($ralph_user_set); + + # set some fields that are created from defaults when written to the DB. + $ralph_set_info->{set_type} = 'HW'; + $ralph_set_info->{set_version} = 0; + + is($ralph_user_set, $ralph_set_info, 'addUserSet: add a new user with dates (some are missing).'); + + # Try to add a bad date. + is( + dies { + $user_set_rs->addUserSet( + params => { + %$otto_set_info5, + set_dates => { open => 100, due => 9, answer => 1000, enable_reduced_scoring => false } + } + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'addUserSet: dates are out of order' + ); + + is( + dies { + $user_set_rs->addUserSet( + params => { %$otto_set_info5, set_dates => { open => 100, due => 900, answer => 800 } }); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'addUserSet: dates are out of order' + ); + # Add a new user set and test that it is merged correctly. + + my $otto_quiz_info = { course_name => 'Precalculus', set_name => 'Quiz #1', username => 'otto' }; + + # Then add a new user set and test that it is merged correctly. + + my $merged_set1 = clone( + ( + grep { + $_->{course_name} eq $otto_quiz_info->{course_name} + && $_->{set_name} eq $otto_quiz_info->{set_name} + } @all_problem_sets + )[0] + ); + + $merged_set1->{username} = $otto_quiz_info->{username}; + + my $new_dates = { + due => $merged_set->{set_dates}{answer} + 1000, + answer => $merged_set->{set_dates}{answer} + 2000 + }; + + $merged_set1->{set_dates}{due} = $new_dates->{due}; + $merged_set1->{set_dates}{answer} = $new_dates->{answer}; + + my $user_set_to_merge = $user_set_rs->addUserSet( + params => { %$otto_quiz_info, set_dates => $new_dates }, + merged => 1 + ); + removeIDs($user_set_to_merge); + # otto_quiz has set_version 0. Need to match to compare. + $merged_set1->{set_version} = 0; + + is($user_set_to_merge, $merged_set1, 'addUserSet: adding a user set with dates to check merging'); + + # Check that adding a user set that is out of order with the problem sets throws an error. + # This set has a due date before the reduced scoring date. + is( + dies { + $user_set_rs->addUserSet( + params => { %$otto_set_info5, set_dates => { due => 1609595640 } }, + merged => 1 + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'addUserSet: user set is out of order with respect to problem set' + ); + + # Check that setting a boolean as 0/1 throws an error + # Currently, this is stripped out if hide_hint is 0. Do we want to check for this? + is( + dies { + $user_set_rs->addUserSet( + params => { %$otto_set_info5, set_params => { hide_hint => 1 } }, + merged => 1 + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addUserSet: boolean valid should be JSON boolean' + ); + + # Update User Set + + my $otto_quiz = $user_set_rs->getUserSet(info => $otto_quiz_info); + removeIDs($otto_quiz); + + # Update the dates + my $updated_dates = { + due => $otto_quiz->{set_dates}{due} + 100, + answer => $otto_quiz->{set_dates}{due} + 1000 + }; + + $otto_quiz->{set_dates}{due} = $updated_dates->{due}; + $otto_quiz->{set_dates}{answer} = $updated_dates->{answer}; + + my $updated_user_quiz = $user_set_rs->updateUserSet(info => $otto_quiz_info, params => $otto_quiz); + removeIDs($updated_user_quiz); + + is($updated_user_quiz, $otto_quiz, 'updateUserSet: update the dates'); + + # Update the params + my $updated_user_set2 = $user_set_rs->updateUserSet( + info => $otto_quiz_info, + params => { set_params => { problem_randorder => true } } + ); + removeIDs($updated_user_set2); + $otto_quiz->{set_params}{problem_randorder} = true; + is($updated_user_set2, $otto_quiz, 'updateUserSet: update the params'); + + # Update a valid field + my $updated_user_set3 = $user_set_rs->updateUserSet( + info => $otto_quiz_info, + params => { set_visible => true } + ); + removeIDs($updated_user_set3); + $otto_quiz->{set_visible} = true; + + is($otto_quiz, $updated_user_set3, 'updateUserSet: update the set visibility'); + + # Try updating an invalid param. + is( + dies { + $user_set_rs->updateUserSet( + info => $otto_quiz_info, + params => { set_params => { not_a_valid_param => 'bad' } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'updateUserSet: try setting a parameter that does not exist' + ); + + # Try updating an invalid date. + is( + dies { + $user_set_rs->updateUserSet( + info => $otto_quiz_info, + params => { set_dates => { open => 1, closed => 2 } } + ); + }, + check_isa('DB::Exception::InvalidField'), + 'updateUserSet: try to update an invalid date field' + ); + + # Test with out of order dates. + is( + dies { + $user_set_rs->updateUserSet( + info => $otto_quiz_info, + params => { set_dates => { open => 100, due => 2, answer => 200 } } + ); + }, + check_isa('DB::Exception::ImproperDateOrder'), + 'updateUserSet: try to update with out of order dates' + ); + + # Try to update a user_set that doesn't exist. + is( + dies { + $user_set_rs->updateUserSet( + info => $otto_set_info5, + params => { set_params => { hide_hint => true } } + ); + }, + check_isa('DB::Exception::UserSetNotInCourse'), + 'updateUserSet: try to update a user set not the in the course' + ); + + # Try to delete a user_set that doesn't exist. + is( + dies { $user_set_rs->deleteUserSet(info => $otto_set_info5); }, + check_isa('DB::Exception::UserSetNotInCourse'), + 'deleteUserSet: try to delete a user set not the in the course' + ); + + # Delete some user sets that were created. + my $deleted_user_set = $user_set_rs->deleteUserSet( + info => { + username => $new_user_set2->{username}, + course_name => $new_user_set2->{course_name}, + set_name => $new_user_set2->{set_name} + } + ); + removeIDs($deleted_user_set); + cleanUndef($deleted_user_set); + removeIDs($new_user_set2); + cleanUndef($new_user_set2); + is($deleted_user_set, $new_user_set2, "deleteUserSet: successfully delete a user set"); +}; done_testing; diff --git a/t/db/008_problem_pools.t b/t/db/008_problem_pools.t index a8a72346..a0bfd264 100644 --- a/t/db/008_problem_pools.t +++ b/t/db/008_problem_pools.t @@ -2,315 +2,319 @@ # This tests the basic database CRUD functions of Problem Pools and Pool Problems . -use warnings; -use strict; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - use Test2::V0; -use YAML::XS qw/LoadFile/; -use Clone qw/clone/; - -use DB::Schema; -use TestUtils qw/loadCSV removeIDs/; - -# Load the database -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -my $problem_pool_rs = $schema->resultset('ProblemPool'); - -# Load pool problems from the csv files. -my @pool_problems_from_file = loadCSV( - "$main::ww3_dir/t/db/sample_data/pool_problems.csv", - { - param_non_neg_int_fields => ['library_id'] - } -); - -# To find the problem pools, remove duplicates and remove the params field. -my @problem_pools_from_file = sort { $a->{pool_name} cmp $b->{pool_name} } @{ clone(\@pool_problems_from_file) }; -my %seen; -@problem_pools_from_file = grep { !$seen{ $_->{pool_name} }++ } @problem_pools_from_file; -for my $pool (@problem_pools_from_file) { - delete $pool->{params}; -} - -# get only problem_pools for Precalculus course. - -my @precalc_pools_from_file = grep { $_->{course_name} eq 'Precalculus' } @{ clone(\@problem_pools_from_file) }; -my @problem_pools_from_db = $problem_pool_rs->getAllProblemPools(); -@problem_pools_from_db = sort { $a->{pool_name} cmp $b->{pool_name} } @problem_pools_from_db; -for my $pool (@problem_pools_from_db) { - removeIDs($pool); -} - -is(\@problem_pools_from_db, \@problem_pools_from_file, 'getAllProblemPools: find all problem pools'); - -my @precalc_pools = $problem_pool_rs->getProblemPools(info => { course_name => 'Precalculus' }); - -for my $pool (@precalc_pools) { - removeIDs($pool); - $pool->{course_name} = 'Precalculus'; -} - -is(\@precalc_pools, \@precalc_pools_from_file, 'getProblemPools: get all problem pools from a single course'); - -# Get a problem pool -my $pool_to_fetch = $problem_pools_from_file[0]; - -my $fetched_pool = $problem_pool_rs->getProblemPool(info => $pool_to_fetch); -removeIDs($fetched_pool); -$fetched_pool->{course_name} = $problem_pools_from_file[0]->{course_name}; - -is($fetched_pool, $pool_to_fetch, 'getProblemPool: get a single pool from a course'); - -# Try to get a problem pool from a course that doesn't exist. -is( - dies { - $problem_pool_rs->getProblemPool( - info => { course_name => 'not existent course', pool_name => 'adding fractions' }); - }, - check_isa('DB::Exception::CourseNotFound'), - 'getProblemPool: get a problem pool from a non-existent course' -); - -# Try to get a problem pool from a course, but the pool doesn't exist. -is( - dies { - $problem_pool_rs->getProblemPool(info => { course_name => 'Arithmetic', pool_name => 'non_existent_pool' }); - }, - check_isa('DB::Exception::PoolNotInCourse'), - 'getProblemPool: get a problem pool from a non-existent course' -); - -# Add a problem pool -my $course_name = 'Arithmetic'; -my $pool_name = 'subtracting fractions'; - -my $pool2 = $problem_pool_rs->addProblemPool( - params => { - course_name => $course_name, - pool_name => $pool_name + +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; + +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json/; + +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use BuildDB qw/loadPermissions addCourses addUsers addSets addProblemPools/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs/; + +my $ww3_dir = curfile->dirname->dirname->dirname; + +dbSubtest 'course users' => sub ($schema) { + # Add the neccessary sample data to the database. + loadPermissions($schema, $ww3_dir); + addCourses($schema, $ww3_dir); + addUsers($schema, $ww3_dir); + addSets($schema, $ww3_dir); + addProblemPools($schema, $ww3_dir); + + my $problem_pool_rs = $schema->resultset('ProblemPool'); + + # Load problem pools and pool problems from the JSON file. + my $course_pool_problems = decode_json($ww3_dir->child('t/db/sample_data/pool_problems.json')->slurp); + my @problem_pools_from_file; + my @precalc_pools_from_file; # Save precalculus problem pools. + my @pool_problems_from_file; + for my $course_info (@$course_pool_problems) { + for my $problem_pool_info (@{ $course_info->{pools} }) { + push(@problem_pools_from_file, + { course_name => $course_info->{course_name}, pool_name => $problem_pool_info->{pool_name} }); + push(@precalc_pools_from_file, $problem_pools_from_file[-1]) + if $course_info->{course_name} eq 'Precalculus'; + + for my $pool_problem (@{ $problem_pool_info->{pool_problems} }) { + push( + @pool_problems_from_file, + { + %$pool_problem, + course_name => $course_info->{course_name}, + pool_name => $problem_pool_info->{pool_name} + } + ); + } + } } -); -removeIDs($pool2); - -is($pool2, { pool_name => $pool_name }, 'addProblemPool: add a new problem pool to a course'); - -# Try to add a pool that already exists. -is( - dies { - $problem_pool_rs->addProblemPool( - params => { course_name => 'Arithmetic', pool_name => 'adding fractions' }); - }, - check_isa('DB::Exception::PoolAlreadyInCourse'), - 'addProblemPool: pool already exists' -); - -# Try to add a pool with an invalid field. -is( - dies { - $problem_pool_rs->addProblemPool( - params => { course_name => 'Arithmetic', pool_name => 'dividing fractions', other_field => 'XXX' }); - }, - check_isa('DBIx::Class::Exception'), - 'addProblemPool: add a pool with non-valid field' -); - -# Update an existing problem pool. -my $updated_pool = { pool_name => 'subtracting fractions with like denominators', }; - -my $updated_pool_from_db = $problem_pool_rs->updateProblemPool( - info => { course_name => 'Arithmetic', pool_name => 'subtracting fractions' }, - params => $updated_pool -); -$updated_pool_from_db->{course_name} = 'Arithmetic'; - -removeIDs($updated_pool_from_db); -$updated_pool->{course_name} = 'Arithmetic'; - -is($updated_pool_from_db, $updated_pool, 'updateProblemPool: update the name of a problem pool'); - -# TODO: Try to update a pool that doesn't exist. - -# Try to get a problem pool from a course that doesn't exist. -is( - dies { - $problem_pool_rs->updateProblemPool( - info => { course_name => 'non_existent_course', pool_name => 'XXXX' }, - parms => $updated_pool - ); - }, - check_isa('DB::Exception::CourseNotFound'), - 'udpateProblemPool: update a problem pool from a non-existent course' -); - -# Try to get a non-existent problem pool from a course. -is( - dies { - $problem_pool_rs->updateProblemPool( - info => { course_name => 'Arithmetic', pool_name => 'non_existent_pool' }, - params => $updated_pool - ); - }, - check_isa('DB::Exception::PoolNotInCourse'), - 'updateProblemPool: update a problem pool from a non-existent course' -); - -# Get all PoolProblems from within a pool -my @pool_problems = $problem_pool_rs->getPoolProblems( - info => { - course_name => 'Precalculus', - pool_name => $precalc_pools_from_file[0]->{pool_name} + @problem_pools_from_file = sort { $a->{pool_name} cmp $b->{pool_name} } @problem_pools_from_file; + + # FIXME: the getAllProblemPools method should not exist and should not be tested. + # Why do you think these "get all" methods are a good idea? + my @problem_pools_from_db = $problem_pool_rs->getAllProblemPools(); + @problem_pools_from_db = sort { $a->{pool_name} cmp $b->{pool_name} } @problem_pools_from_db; + for my $pool (@problem_pools_from_db) { + removeIDs($pool); } -); - -# Get a PoolProblem (a problem within a ProblemPool). -my $prob2 = $pool_problems_from_file[0]; - -my $pool_problem2 = $problem_pool_rs->getPoolProblem(info => $prob2); - -is($prob2->{library_id}, $pool_problem2->{library_id}, 'getPoolProblem: get a single problem from a problem pool'); + is(\@problem_pools_from_db, \@problem_pools_from_file, 'getAllProblemPools: find all problem pools'); -# Get a random PoolProblem. -my $random_prob = $problem_pool_rs->getPoolProblem( - info => { - course_name => $prob2->{course_name}, - pool_name => $prob2->{pool_name} + # Get all precalculus problem pools. + my @precalc_pools = $problem_pool_rs->getProblemPools(info => { course_name => 'Precalculus' }); + for my $pool (@precalc_pools) { + removeIDs($pool); + $pool->{course_name} = 'Precalculus'; } -); - -my @probs3 = - grep { $_->{course_name} eq $prob2->{course_name} and $_->{pool_name} eq $prob2->{pool_name} } - @pool_problems_from_file; -my @lib_ids = map { $_->{params}->{library_id} } @probs3; -my @arr = grep { $_ == $random_prob->{params}->{library_id} } @lib_ids; - -ok(scalar(@arr) == 1, 'getPoolProblem: get a random problem from a problem pool'); - -# Add a Problem to a pool. -my $prob_to_add->{params} = { library_id => 8332 }; -my $added_problem = $problem_pool_rs->addProblemToPool(params => { %$updated_pool, %$prob_to_add }); - -is( - $prob_to_add->{params}->{library_id}, - $added_problem->{params}->{library_id}, - 'addProblemToPool: adding a problem to an existing pool.' -); - -# Check that adding a problem to a non-existence course fails. -is( - dies { - $problem_pool_rs->addProblemToPool( - params => { course_name => 'non_existing_course', pool_name => 'adding fractions', %$prob_to_add }); - }, - check_isa('DB::Exception::CourseNotFound'), - 'addProblemToPool: try to add to a nonexisting course' -); - -# Check that adding a problem to a non-existence pool fails. -is( - dies { - $problem_pool_rs->addProblemToPool(params => - { course_name => $updated_pool->{course_name}, pool_name => 'non_existent_pool_name', %$prob_to_add }); - }, - check_isa('DB::Exception::PoolNotInCourse'), - 'addProblemToPool: try to add to a nonexisting pool' -); - -# Update a pool problem. -my $course_pool_problem_info = {%$updated_pool_from_db}; -$course_pool_problem_info->{pool_problem_id} = $added_problem->{pool_problem_id}; - -my $updated_library_id = 2839; - -my $updated_pool_problem = $problem_pool_rs->updatePoolProblem( - info => $course_pool_problem_info, - params => { params => { library_id => $updated_library_id } } -); - -is( - $updated_library_id, - $updated_pool_problem->{params}->{library_id}, - 'updatePoolProblem: update an existing problem in an existing pool.' -); - -# Check that updating a problem to a non-existence course fails. -is( - dies { - $problem_pool_rs->updatePoolProblem( - info => { - course_name => 'non_existing_course', - pool_name => 'adding fractions', - pool_problem_id => $added_problem->{pool_problem_id} - }, - params => { params => { library_id => $updated_library_id } } - ); - }, - check_isa('DB::Exception::CourseNotFound'), - 'updatePoolProblem: try to update a nonexisting course' -); - -# Check that updating a problem to a non-existence course fails. -is( - dies { - $problem_pool_rs->updatePoolProblem( - info => { - course_name => $updated_pool->{course_name}, - pool_name => 'non_existent_pool_name', - pool_problem_id => $added_problem->{pool_problem_id} - }, - params => { library_id => $updated_library_id } - ); - }, - check_isa('DB::Exception::PoolNotInCourse'), - 'updatePoolProblem: try to update a nonexisting pool' -); - -# Check that updating a problem to a non-existing problem fails. -is( - dies { - $problem_pool_rs->updatePoolProblem( - info => { - course_name => $updated_pool->{course_name}, - pool_name => $updated_pool->{pool_name}, - pool_problem_id => -999 - }, - params => { library_id => $updated_library_id } - ); - }, - check_isa('DB::Exception::PoolProblemNotInPool'), - 'updatePoolProblem: try to update a nonexisting problem' -); - -# Delete a problem pool -my $pool_to_delete = $problem_pool_rs->deleteProblemPool(info => $updated_pool); -removeIDs($pool_to_delete); -$pool_to_delete->{course_name} = 'Arithmetic'; -is($pool_to_delete, $updated_pool, 'deleteProblemPool: delete an existing problem pool'); - -# Ensure that the problem_pool table is restored. -@problem_pools_from_db = $problem_pool_rs->getAllProblemPools(); -@problem_pools_from_db = sort { $a->{pool_name} cmp $b->{pool_name} } @problem_pools_from_db; -for my $pool (@problem_pools_from_db) { - removeIDs($pool); -} - -is(\@problem_pools_from_db, \@problem_pools_from_file, 'check: Ensure that the problem_pool table is restored.'); + is(\@precalc_pools, \@precalc_pools_from_file, 'getProblemPools: get all problem pools from a single course'); + + # Get a problem pool + my $pool_to_fetch = $problem_pools_from_file[0]; + + my $fetched_pool = $problem_pool_rs->getProblemPool(info => $pool_to_fetch); + removeIDs($fetched_pool); + $fetched_pool->{course_name} = $problem_pools_from_file[0]->{course_name}; + + is($fetched_pool, $pool_to_fetch, 'getProblemPool: get a single pool from a course'); + + # Try to get a problem pool from a course that doesn't exist. + is( + dies { + $problem_pool_rs->getProblemPool( + info => { course_name => 'not existent course', pool_name => 'adding fractions' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getProblemPool: get a problem pool from a non-existent course' + ); + + # Try to get a problem pool from a course, but the pool doesn't exist. + is( + dies { + $problem_pool_rs->getProblemPool( + info => { course_name => 'Arithmetic', pool_name => 'non_existent_pool' }); + }, + check_isa('DB::Exception::PoolNotInCourse'), + 'getProblemPool: get a problem pool from a non-existent course' + ); + + # Add a problem pool + my $course_name = 'Arithmetic'; + my $pool_name = 'subtracting fractions'; + + my $pool2 = $problem_pool_rs->addProblemPool( + params => { + course_name => $course_name, + pool_name => $pool_name + } + ); + removeIDs($pool2); + + is($pool2, { pool_name => $pool_name }, 'addProblemPool: add a new problem pool to a course'); + + # Try to add a pool that already exists. + is( + dies { + $problem_pool_rs->addProblemPool( + params => { course_name => 'Arithmetic', pool_name => 'adding fractions' }); + }, + check_isa('DB::Exception::PoolAlreadyInCourse'), + 'addProblemPool: pool already exists' + ); + + # Try to add a pool with an invalid field. + is( + dies { + $problem_pool_rs->addProblemPool( + params => { course_name => 'Arithmetic', pool_name => 'dividing fractions', other_field => 'XXX' }); + }, + check_isa('DBIx::Class::Exception'), + 'addProblemPool: add a pool with non-valid field' + ); + + # Update an existing problem pool. + my $updated_pool = { pool_name => 'subtracting fractions with like denominators', }; + + my $updated_pool_from_db = $problem_pool_rs->updateProblemPool( + info => { course_name => 'Arithmetic', pool_name => 'subtracting fractions' }, + params => $updated_pool + ); + $updated_pool_from_db->{course_name} = 'Arithmetic'; + + removeIDs($updated_pool_from_db); + $updated_pool->{course_name} = 'Arithmetic'; + + is($updated_pool_from_db, $updated_pool, 'updateProblemPool: update the name of a problem pool'); + + # TODO: Try to update a pool that doesn't exist. + + # Try to get a problem pool from a course that doesn't exist. + is( + dies { + $problem_pool_rs->updateProblemPool( + info => { course_name => 'non_existent_course', pool_name => 'XXXX' }, + parms => $updated_pool + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'udpateProblemPool: update a problem pool from a non-existent course' + ); + + # Try to get a non-existent problem pool from a course. + is( + dies { + $problem_pool_rs->updateProblemPool( + info => { course_name => 'Arithmetic', pool_name => 'non_existent_pool' }, + params => $updated_pool + ); + }, + check_isa('DB::Exception::PoolNotInCourse'), + 'updateProblemPool: update a problem pool from a non-existent course' + ); + + # Get all PoolProblems from within a pool + my @pool_problems = $problem_pool_rs->getPoolProblems( + info => { + course_name => 'Precalculus', + pool_name => $precalc_pools_from_file[0]->{pool_name} + } + ); + + # Get a PoolProblem (a problem within a ProblemPool). + my $prob2 = $pool_problems_from_file[0]; + + my $pool_problem2 = $problem_pool_rs->getPoolProblem(info => $prob2); + + is( + $prob2->{library_id}, + $pool_problem2->{library_id}, + 'getPoolProblem: get a single problem from a problem pool' + ); + + # Get a random PoolProblem. + my $random_prob = $problem_pool_rs->getPoolProblem( + info => { + course_name => $prob2->{course_name}, + pool_name => $prob2->{pool_name} + } + ); + + my @probs3 = + grep { $_->{course_name} eq $prob2->{course_name} and $_->{pool_name} eq $prob2->{pool_name} } + @pool_problems_from_file; + my @lib_ids = map { $_->{params}{library_id} } @probs3; + my @arr = grep { $_ == $random_prob->{params}{library_id} } @lib_ids; + + ok(scalar(@arr) == 1, 'getPoolProblem: get a random problem from a problem pool'); + + # Add a Problem to a pool. + my $prob_to_add->{params} = { library_id => 8332 }; + my $added_problem = $problem_pool_rs->addProblemToPool(params => { %$updated_pool, %$prob_to_add }); + + is( + $prob_to_add->{params}{library_id}, + $added_problem->{params}{library_id}, + 'addProblemToPool: adding a problem to an existing pool.' + ); + + # Check that adding a problem to a non-existence course fails. + is( + dies { + $problem_pool_rs->addProblemToPool( + params => { course_name => 'non_existing_course', pool_name => 'adding fractions', %$prob_to_add }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'addProblemToPool: try to add to a nonexisting course' + ); + + # Check that adding a problem to a non-existence pool fails. + is( + dies { + $problem_pool_rs->addProblemToPool( + params => { + course_name => $updated_pool->{course_name}, + pool_name => 'non_existent_pool_name', + %$prob_to_add + } + ); + }, + check_isa('DB::Exception::PoolNotInCourse'), + 'addProblemToPool: try to add to a nonexisting pool' + ); + + # Update a pool problem. + my $course_pool_problem_info = {%$updated_pool_from_db}; + $course_pool_problem_info->{pool_problem_id} = $added_problem->{pool_problem_id}; + + my $updated_library_id = 2839; + + my $updated_pool_problem = $problem_pool_rs->updatePoolProblem( + info => $course_pool_problem_info, + params => { params => { library_id => $updated_library_id } } + ); + + is( + $updated_library_id, + $updated_pool_problem->{params}{library_id}, + 'updatePoolProblem: update an existing problem in an existing pool.' + ); + + # Check that updating a problem to a non-existence course fails. + is( + dies { + $problem_pool_rs->updatePoolProblem( + info => { + course_name => 'non_existing_course', + pool_name => 'adding fractions', + pool_problem_id => $added_problem->{pool_problem_id} + }, + params => { params => { library_id => $updated_library_id } } + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'updatePoolProblem: try to update a nonexisting course' + ); + + # Check that updating a problem to a non-existence course fails. + is( + dies { + $problem_pool_rs->updatePoolProblem( + info => { + course_name => $updated_pool->{course_name}, + pool_name => 'non_existent_pool_name', + pool_problem_id => $added_problem->{pool_problem_id} + }, + params => { library_id => $updated_library_id } + ); + }, + check_isa('DB::Exception::PoolNotInCourse'), + 'updatePoolProblem: try to update a nonexisting pool' + ); + + # Check that updating a problem to a non-existing problem fails. + is( + dies { + $problem_pool_rs->updatePoolProblem( + info => { + course_name => $updated_pool->{course_name}, + pool_name => $updated_pool->{pool_name}, + pool_problem_id => -999 + }, + params => { library_id => $updated_library_id } + ); + }, + check_isa('DB::Exception::PoolProblemNotInPool'), + 'updatePoolProblem: try to update a nonexisting problem' + ); + + # Delete a problem pool + my $pool_to_delete = $problem_pool_rs->deleteProblemPool(info => $updated_pool); + removeIDs($pool_to_delete); + $pool_to_delete->{course_name} = 'Arithmetic'; + is($pool_to_delete, $updated_pool, 'deleteProblemPool: delete an existing problem pool'); +}; done_testing; diff --git a/t/db/009_problems.t b/t/db/009_problems.t index 86c214b6..986172f0 100644 --- a/t/db/009_problems.t +++ b/t/db/009_problems.t @@ -2,367 +2,308 @@ # This tests the basic database CRUD functions of problems. -use warnings; -use strict; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - use Test2::V0; -use Clone qw/clone/; -use YAML::XS qw/LoadFile/; - -use DB::Schema; -use TestUtils qw/loadCSV removeIDs/; -use DB::Utils qw/updateAllFields/; - -# Load the database -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -my $problem_rs = $schema->resultset('SetProblem'); -my $problem_set_rs = $schema->resultset('ProblemSet'); - -# Load all problems from the CVS files. -my @problems_from_csv = loadCSV("$main::ww3_dir/t/db/sample_data/problems.csv"); - -# Filter out precalc problems -my @precalc_problems = grep { $_->{course_name} eq 'Precalculus' } @problems_from_csv; - -# Filter out 'HW #1' -my @precalc_problems1 = grep { $_->{set_name} eq 'HW #1' } @precalc_problems; - -for my $problem (@problems_from_csv) { - delete $problem->{course_name}; -} - -my @all_problems = map { clone($_) } @problems_from_csv; - -my @problems_from_db = $problem_rs->getGlobalProblems; -for my $problem (@problems_from_db) { - removeIDs($problem); -} - -is(\@problems_from_db, \@all_problems, 'getGlobalProblems: get all problems'); - -# Get all problems from one course. -my @precalc_problems_from_db = $problem_rs->getProblems(info => { course_name => 'Precalculus' }); -for my $problem (@precalc_problems_from_db) { - removeIDs($problem); -} -# Try to get all problems from a non existent course +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -is( - dies { $problem_rs->getProblems(info => { course_name => 'non existent course' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'getProblems: non-existent course' -); - -# Try to get all problems from a non-existent set_id - -is( - dies { $problem_rs->getProblems(info => { course_id => 999999 }); }, - check_isa('DB::Exception::CourseNotFound'), - 'getProblems: non-existent course_id' -); +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json/; +use Clone qw/clone/; -for my $problem (@precalc_problems) { - delete $problem->{set_name}; -} +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; -is(\@precalc_problems_from_db, \@precalc_problems, 'getSetProblems: get all problems from one course'); +use BuildDB qw/addCourses addSets addProblems/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs/; +use DB::Utils qw/updateAllFields/; -# Get all problems in one course from one set. -my @set_problems1 = $problem_rs->getSetProblems(info => { course_name => 'Precalculus', set_name => 'HW #1' }); +my $ww3_dir = curfile->dirname->dirname->dirname; -for my $problem (@set_problems1) { - removeIDs($problem); -} +dbSubtest 'course users' => sub ($schema) { + # Add the neccessary sample data to the database. + addCourses($schema, $ww3_dir); + addSets($schema, $ww3_dir); + addProblems($schema, $ww3_dir); -is(\@set_problems1, \@precalc_problems1, 'getSetProblems: get all problems from one set'); + my $problem_rs = $schema->resultset('SetProblem'); -# Try to get problems from a non-existing course. -is( - dies { $problem_rs->getSetProblems(info => { course_name => 'non_existing_course', set_name => 'HW #1' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'getSetProblems: get problems from non-existing course' -); + # Load all problems from the the JSON file. + my $course_set_problems = decode_json($ww3_dir->child('t/db/sample_data/problems.json')->slurp); + my @all_problems; + my @precalc_problems; + my @precalc_problems1; + for my $course_info (@$course_set_problems) { + for my $set_info (@{ $course_info->{sets} }) { + for (@{ $set_info->{problems} }) { + push(@all_problems, { %$_, set_name => $set_info->{set_name} }); -# Try to get problems from a non-existing set. -is( - dies { $problem_rs->getSetProblems(info => { course_name => 'Precalculus', set_name => 'HW #999' }); }, - check_isa('DB::Exception::SetNotInCourse'), - 'getSetProblems: get problems from non-existing set' -); + # Separate out precalculus problems. + push(@precalc_problems, $_) if $course_info->{course_name} eq 'Precalculus'; -# Get a single problem from a course. -my $set_problem = $problem_rs->getSetProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #1', - problem_number => $problems_from_csv[0]->{problem_number} + # Separate out precalculus 'HW #1' problems. + push(@precalc_problems1, $_) + if $course_info->{course_name} eq 'Precalculus' && $set_info->{set_name} eq 'HW #1'; + } + } } -); -removeIDs($set_problem); - -my $expected_problem = { %{ $problems_from_csv[0] } }; # Copy the first problem -delete $expected_problem->{set_name}; -delete $expected_problem->{course_name}; -is($set_problem, $expected_problem, 'getSetProblem: get a single problem from a set in a given course'); - -# Add a problem to an existing set. -my $new_problem = { - problem_number => 4, - problem_params => { - library_id => 13245, - weight => 1 + # FIXME: The getGlobalProblems method should not exist and should not be tested. + my @problems_from_db = $problem_rs->getGlobalProblems; + for my $problem (@problems_from_db) { + removeIDs($problem); } -}; + is(\@problems_from_db, \@all_problems, 'getGlobalProblems: get all problems'); -my $prob1 = $problem_rs->addSetProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #1', - %$new_problem + # FIXME: The getProblems method should not exist and should not be tested. + # Get all problems from one course. + my @precalc_problems_from_db = $problem_rs->getProblems(info => { course_name => 'Precalculus' }); + for my $problem (@precalc_problems_from_db) { + removeIDs($problem); } -); - -my $prob_id = $prob1->{set_problem_id}; -removeIDs($prob1); - -is($prob1, $new_problem, 'addSetProblem: add a valid problem to a set'); - -# Add a problem and make sure the problem number is working. -# Determine the largest current problem number. - -my @hw1_probs = $problem_rs->getSetProblems( - info => { - course_name => 'Precalculus', - set_name => 'HW #1' + is(\@precalc_problems_from_db, \@precalc_problems, 'getSetProblems: get all problems from one course'); + + # Try to get all problems from a non existent course + is( + dies { $problem_rs->getProblems(info => { course_name => 'non existent course' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getProblems: non-existent course' + ); + + # Try to get all problems from a non-existent set_id + is( + dies { $problem_rs->getProblems(info => { course_id => 999999 }); }, + check_isa('DB::Exception::CourseNotFound'), + 'getProblems: non-existent course_id' + ); + + # Get all problems in one course from one set. + my @set_problems1 = $problem_rs->getSetProblems(info => { course_name => 'Precalculus', set_name => 'HW #1' }); + + for my $problem (@set_problems1) { + removeIDs($problem); } -); - -my @prob_nums = map { $_->{problem_number} } @hw1_probs; -my $max = $prob_nums[0]; -for my $i (1 .. $#prob_nums) { $max = $prob_nums[$i] if $prob_nums[$i] > $max; } -my $prob2_from_db = $problem_rs->addSetProblem( - params => { + is(\@set_problems1, \@precalc_problems1, 'getSetProblems: get all problems from one set'); + + # Try to get set problems from a non-existing course. + is( + dies { + $problem_rs->getSetProblems(info => { course_name => 'non_existing_course', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getSetProblems: get problems from non-existing course' + ); + + # Try to get set problems from a non-existing set. + is( + dies { + $problem_rs->getSetProblems(info => { course_name => 'Precalculus', set_name => 'HW #999' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'getSetProblems: get problems from non-existing set' + ); + + # Get a single problem from a course. + my $set_problem = $problem_rs->getSetProblem( + info => { + course_name => $course_set_problems->[0]{course_name}, + set_name => $course_set_problems->[0]{sets}[0]{set_name}, + problem_number => $course_set_problems->[0]{sets}[0]{problems}[0]{problem_number} + } + ); + removeIDs($set_problem); + + my $expected_problem = { %{ $course_set_problems->[0]{sets}[0]{problems}[0] } }; # Copy the first problem + + is($set_problem, $expected_problem, 'getSetProblem: get a single problem from a set in a given course'); + + # Add a problem to an existing set. + my $new_problem = { problem_number => 4, problem_params => { library_id => 13245, weight => 1 } }; + + my $prob1 = + $problem_rs->addSetProblem(params => { course_name => 'Precalculus', set_name => 'HW #1', %$new_problem }); + + my $prob_id = $prob1->{set_problem_id}; + removeIDs($prob1); + + is($prob1, $new_problem, 'addSetProblem: add a valid problem to a set'); + + # Add a problem and make sure the problem number is working. + # Determine the largest current problem number. + + my @hw1_probs = $problem_rs->getSetProblems(info => { course_name => 'Precalculus', set_name => 'HW #1' }); + + my @prob_nums = map { $_->{problem_number} } @hw1_probs; + my $max = $prob_nums[0]; + for my $i (1 .. $#prob_nums) { $max = $prob_nums[$i] if $prob_nums[$i] > $max; } + + my $prob2_from_db = $problem_rs->addSetProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #1', + problem_params => { file_path => 'path/to/problem.pg' } + } + ); + removeIDs($prob2_from_db); + + my $prob2 = { + 'problem_params' => { 'file_path' => 'path/to/problem.pg', 'weight' => 1 }, + 'problem_number' => $max + 1, + }; + + is($prob2_from_db, $prob2, 'addSetProblem: add a set problem and ensure the problem number is correct.'); + + # Try to add a problem to a non-existent course + is( + dies { + $problem_rs->addSetProblem(params => { course_name => 'Non existent course', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'addSetProblem: try to add a problem to a non-existent course' + ); + + # Try to add a problem to a non-existent course_id + is( + dies { $problem_rs->addSetProblem(params => { course_id => 999999, set_name => 'HW #1' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'addSetProblem: try to add a problem to a non-existent course_id' + ); + + # Try to add a problem to a non-existent set + is( + dies { + $problem_rs->addSetProblem(params => { course_name => 'Precalculus', set_name => 'HW #99999' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'addSetProblem: try to add a problem to a non-existent set' + ); + + # Try to add a problem to a non-existent set + is( + dies { $problem_rs->addSetProblem(params => { course_name => 'Precalculus', set_id => 999999 }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'addSetProblem: try to add a problem to a non-existent set_id' + ); + + # Try to add a problem without information about the file_path, library_id or problem_pool_id + is( + dies { + $problem_rs->addSetProblem( + params => { course_name => 'Precalculus', set_name => 'HW #1', problem_params => {} }); + }, + check_isa('DB::Exception::FieldsNeeded'), + "addSetProblem: try to add a problem without information about the file_path, etc." + ); + + # Note: we may want to not have the following in the future, but currently its okay. + # Try to add a problem with both information about the file_path, and library_id . + + my $set_prob_params2 = { course_name => 'Precalculus', set_name => 'HW #1', - problem_params => { file_path => 'path/to/problem.pg' } - } -); -removeIDs($prob2_from_db); - -my $prob2 = { - 'problem_params' => { - 'file_path' => 'path/to/problem.pg', - 'weight' => 1 - }, - 'problem_number' => $max + 1, -}; - -is($prob2_from_db, $prob2, 'addSetProblem: add a set problem and ensure the problem number is correct.'); - -# Try to add a problem to a non-existent course -is( - dies { $problem_rs->addSetProblem(params => { course_name => 'Non existent course', set_name => 'HW #1' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'addSetProblem: try to add a problem to a non-existent course' -); - -# Try to add a problem to a non-existent course_id -is( - dies { $problem_rs->addSetProblem(params => { course_id => 999999, set_name => 'HW #1' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'addSetProblem: try to add a problem to a non-existent course_id' -); - -# Try to add a problem to a non-existent set -is( - dies { $problem_rs->addSetProblem(params => { course_name => 'Precalculus', set_name => 'HW #99999' }); }, - check_isa('DB::Exception::SetNotInCourse'), - 'addSetProblem: try to add a problem to a non-existent set' -); - -# Try to add a problem to a non-existent set -is( - dies { $problem_rs->addSetProblem(params => { course_name => 'Precalculus', set_id => 999999 }); }, - check_isa('DB::Exception::SetNotInCourse'), - 'addSetProblem: try to add a problem to a non-existent set_id' -); - -# Try to add a problem without information about the file_path, library_id or problem_pool_id -is( - dies { - $problem_rs->addSetProblem( - params => { course_name => 'Precalculus', set_name => 'HW #1', problem_params => {} }); - }, - check_isa('DB::Exception::FieldsNeeded'), - "addSetProblem: try to add a problem without information about the file_path, etc." -); - -# Note: we may want to not have the following in the future, but currently its okay. -# Try to add a problem with both information about the file_path, and library_id . - -my $set_prob_params2 = { - course_name => 'Precalculus', - set_name => 'HW #1', - problem_params => { - file_path => 'this_is_a_path', - library_id => 123, - weight => 1 - } + problem_params => { file_path => 'this_is_a_path', library_id => 123, weight => 1 } + }; + + my $set_problem2 = $problem_rs->addSetProblem(params => $set_prob_params2); + # Make a copy to delete later. + my $set_problem_to_delete = clone $set_problem2; + delete $set_prob_params2->{course_name}; + delete $set_prob_params2->{set_name}; + + removeIDs($set_problem2); + delete $set_problem2->{problem_number}; + + is($set_problem2, $set_prob_params2, 'addSetProblem: adding a problem with both file_path and library_id'); + + # Update a problem + my $updated_params = { problem_number => 99, problem_params => { weight => 2 } }; + + my $all_params = updateAllFields($new_problem, $updated_params); + my $updated_problem = $problem_rs->updateSetProblem( + info => { course_name => 'Precalculus', set_name => 'HW #1', set_problem_id => $prob_id }, + params => $updated_params + ); + removeIDs($updated_problem); + + is($updated_problem, $all_params, 'updateProblem: update a problem'); + + # Try to update a problem in a non-existent course + is( + dies { + $problem_rs->updateSetProblem(info => { course_name => 'Non existent course', set_name => 'HW #1' }); + }, + check_isa('DB::Exception::CourseNotFound'), + 'updateSetProblem: try to update a problem to a non-existent course' + ); + + # Try to update a problem to with a non-existent course_id + is( + dies { $problem_rs->updateSetProblem(info => { course_id => 999999, set_name => 'HW #1' }); }, + check_isa('DB::Exception::CourseNotFound'), + 'updateSetProblem: try to update a problem to a non-existent course_id' + ); + + # Try to update a problem to a non-existent set + is( + dies { + $problem_rs->updateSetProblem(info => { course_name => 'Precalculus', set_name => 'HW #99999' }); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'updateSetProblem: try to update a problem to a non-existent set' + ); + + # Try to update a problem to a non-existent set_id + is( + dies { $problem_rs->updateSetProblem(info => { course_name => 'Precalculus', set_id => 999999 }); }, + check_isa('DB::Exception::SetNotInCourse'), + 'updateSetProblem: try to update a problem to a non-existent set_id' + ); + + # Try to update a problem with a negative problem number. + is( + dies { + $problem_rs->updateSetProblem( + info => { course_name => 'Precalculus', set_name => 'HW #1', problem_number => 1, }, + params => { problem_number => -9 } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'updateSetProblem: try to update a problem with a non positive integer problem_number' + ); + + # Try to update a problem without information about the file_path, library_id or problem_pool_id + is( + dies { + $problem_rs->updateSetProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #1', + problem_params => { file_path => 'this_is_a_path', library_id => 123 } + } + ); + }, + check_isa('DB::Exception::ParametersNeeded'), + 'updateSetProblem: try to add a problem with too much library info' + ); + + # Delete a problem from a set + my $deleted_problem = + $problem_rs->deleteSetProblem( + info => { course_name => 'Precalculus', set_name => 'HW #1', problem_number => 99 }); + removeIDs($deleted_problem); + + is($deleted_problem, $updated_problem, 'deleteSetProblem: delete one problem in an existing set.'); + + my $deleted_problem2 = $problem_rs->deleteSetProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #1', + problem_number => $set_problem_to_delete->{problem_number}, + } + ); + is($deleted_problem2, $set_problem_to_delete, 'deleteSetProblem: delete another problem.'); }; -my $set_problem2 = $problem_rs->addSetProblem(params => $set_prob_params2); -# Make a copy to delete later. -my $set_problem_to_delete = clone $set_problem2; -delete $set_prob_params2->{course_name}; -delete $set_prob_params2->{set_name}; - -removeIDs($set_problem2); -delete $set_problem2->{problem_number}; - -is($set_problem2, $set_prob_params2, 'addSetProblem: adding a problem with both file_path and library_id'); - -# Update a problem -my $updated_params = { - problem_number => 99, - problem_params => { - weight => 2 - } -}; - -my $all_params = updateAllFields($new_problem, $updated_params); -my $updated_problem = $problem_rs->updateSetProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #1', - set_problem_id => $prob_id - }, - params => $updated_params -); -removeIDs($updated_problem); - -is($updated_problem, $all_params, 'updateProblem: update a problem'); - -# Try to update a problem in a non-existent course -is( - dies { $problem_rs->updateSetProblem(info => { course_name => 'Non existent course', set_name => 'HW #1' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'updateSetProblem: try to update a problem to a non-existent course' -); - -# Try to update a problem to with a non-existent course_id -is( - dies { $problem_rs->updateSetProblem(info => { course_id => 999999, set_name => 'HW #1' }); }, - check_isa('DB::Exception::CourseNotFound'), - 'updateSetProblem: try to update a problem to a non-existent course_id' -); - -# Try to update a problem to a non-existent set -is( - dies { $problem_rs->updateSetProblem(info => { course_name => 'Precalculus', set_name => 'HW #99999' }); }, - check_isa('DB::Exception::SetNotInCourse'), - 'updateSetProblem: try to update a problem to a non-existent set' -); - -# Try to update a problem to a non-existent set_id -is( - dies { $problem_rs->updateSetProblem(info => { course_name => 'Precalculus', set_id => 999999 }); }, - check_isa('DB::Exception::SetNotInCourse'), - 'updateSetProblem: try to update a problem to a non-existent set_id' -); - -# Try to update a problem with a negative problem number. -is( - dies { - $problem_rs->updateSetProblem( - info => { course_name => 'Precalculus', set_name => 'HW #1', problem_number => 1, }, - params => { problem_number => -9 } - ); - }, - check_isa('DB::Exception::InvalidParameter'), - 'updateSetProblem: try to update a problem with a non positive integer problem_number' -); - -# Try to update a problem without information about the file_path, library_id or problem_pool_id -is( - dies { - $problem_rs->updateSetProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #1', - problem_params => { file_path => 'this_is_a_path', library_id => 123 } - } - ); - }, - check_isa('DB::Exception::ParametersNeeded'), - 'updateSetProblem: try to add a problem with too much library info' -); - -# Delete a problem from a set -my $deleted_problem = $problem_rs->deleteSetProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #1', - problem_number => 99, - } -); -removeIDs($deleted_problem); - -is($deleted_problem, $updated_problem, 'deleteSetProblem: delete one problem in an existing set.'); - -my $deleted_problem2 = $problem_rs->deleteSetProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #1', - problem_number => $set_problem_to_delete->{problem_number}, - } -); -is($deleted_problem2, $set_problem_to_delete, 'deleteSetProblem: delete another problem.'); - -my $deleted_problem3 = $problem_rs->deleteSetProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #1', - problem_number => $prob2->{problem_number} - } -); -removeIDs($deleted_problem3); -is($deleted_problem3, $prob2, 'deleteSetProblem: delete another problem.'); - -# Make sure the set_problem table is returned to its orginal state. -@problems_from_db = $problem_rs->getGlobalProblems; -for my $problem (@problems_from_db) { - removeIDs($problem); -} - -is(\@problems_from_db, \@all_problems, 'The set_problems table is returned to its original state.'); - -# Ensure that the set_problems table is restored. -@problems_from_db = $problem_rs->getGlobalProblems; -for my $problem (@problems_from_db) { - removeIDs($problem); -} - -is(\@problems_from_db, \@all_problems, 'check: Ensure that the set_problems table is restored.'); - done_testing; diff --git a/t/db/010_user_problems.t b/t/db/010_user_problems.t index 4d58fc70..9a7049e0 100644 --- a/t/db/010_user_problems.t +++ b/t/db/010_user_problems.t @@ -2,872 +2,790 @@ # This tests the basic database CRUD functions of user problems. -use warnings; -use strict; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +use Test2::V0; -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -use Test2::V0; +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json/; use Clone qw/clone/; -use YAML::XS qw/LoadFile/; - -use DB::Schema; -use TestUtils qw/loadCSV removeIDs/; - -# Set up the database. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); - -my $config = LoadFile($config_file); - -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); -# $schema->storage->debug(1); # print out the SQL commands. - -# Helpful for sorting user problems: - -sub user_prob_sort_fxn { - return - $a->{course_name} cmp $b->{course_name} - || $a->{set_name} cmp $b->{set_name} - || $a->{username} cmp $b->{username} - || $a->{problem_number} <=> $b->{problem_number}; -} - -my $user_problem_rs = $schema->resultset('UserProblem'); - -# Load problems and user problems from the CSV files. -my @user_problems_from_csv = loadCSV("$main::ww3_dir/t/db/sample_data/user_problems.csv"); -for my $user_problem (@user_problems_from_csv) { - $user_problem->{status} = 1 unless defined($user_problem->{status}); - $user_problem->{problem_params} = {} unless defined($user_problem->{problem_params}); - $user_problem->{problem_version} = 1 unless defined($user_problem->{problem_version}); -} -# Sort before comparing -@user_problems_from_csv = sort user_prob_sort_fxn @user_problems_from_csv; - -my @problems_from_csv = loadCSV("$main::ww3_dir/t/db/sample_data/problems.csv"); -for my $problem (@problems_from_csv) { - $problem->{status} = 1 unless defined($problem->{status}); - $problem->{problem_version} = 1 unless defined($problem->{problem_version}); -} - -my @merged_problems_from_csv = (); -for my $user_problem (@user_problems_from_csv) { - my $problem = clone( - ( - grep { - $_->{course_name} eq $user_problem->{course_name} - && $_->{set_name} eq $user_problem->{set_name} - && $_->{problem_number} == $user_problem->{problem_number} - } @problems_from_csv - )[0] - ); - # Override the following fields from user problems. - for my $key (qw/seed status problem_version username/) { - $problem->{$key} = $user_problem->{$key} if defined($user_problem->{$key}); +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use BuildDB qw/loadPermissions addCourses addUsers addSets addProblems addUserSets addUserProblems/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs/; + +my $ww3_dir = curfile->dirname->dirname->dirname; + +dbSubtest 'course users' => sub ($schema) { + # Add the neccessary sample data to the database. + loadPermissions($schema, $ww3_dir); + addCourses($schema, $ww3_dir); + addUsers($schema, $ww3_dir); + addSets($schema, $ww3_dir); + addProblems($schema, $ww3_dir); + addUserSets($schema, $ww3_dir); + addUserProblems($schema, $ww3_dir); + + # User problem sort function. + sub user_prob_sort_fcn { + return + $a->{course_name} cmp $b->{course_name} + || $a->{set_name} cmp $b->{set_name} + || $a->{username} cmp $b->{username} + || $a->{problem_number} <=> $b->{problem_number}; } - # Override any parameters from user problems. - for my $key (keys %{ $user_problem->{problem_params} }) { - $problem->{problem_params}->{$key} = $user_problem->{problem_params}->{$key} - if defined $problem->{problem_params}->{$key}; + + my $user_problem_rs = $schema->resultset('UserProblem'); + + # Load user problems from the JSON file. + my $course_set_problem_user_problems = + decode_json($ww3_dir->child('t/db/sample_data/user_problems.json')->slurp); + my @user_problems_from_json; + for my $course_info (@$course_set_problem_user_problems) { + for my $set_info (@{ $course_info->{sets} }) { + for my $problem_info (@{ $set_info->{problems} }) { + for my $user_info (@{ $problem_info->{users} }) { + $user_info->{user_problem}{course_name} = $course_info->{course_name}; + $user_info->{user_problem}{set_name} = $set_info->{set_name}; + $user_info->{user_problem}{problem_number} = $problem_info->{problem_number}; + $user_info->{user_problem}{username} = $user_info->{username}; + $user_info->{user_problem}{status} //= 1; + $user_info->{user_problem}{problem_params} //= {}; + $user_info->{user_problem}{problem_version} //= 1; + push(@user_problems_from_json, $user_info->{user_problem}); + } + } + } } - push(@merged_problems_from_csv, $problem); -} -# Get all user problems. + # Sort before comparing. + @user_problems_from_json = sort user_prob_sort_fcn @user_problems_from_json; + + # Load all problems from the the JSON file. + my $course_set_problems = decode_json($ww3_dir->child('t/db/sample_data/problems.json')->slurp); + my @problems_from_json; + for my $course_info (@$course_set_problems) { + for my $set_info (@{ $course_info->{sets} }) { + for my $problem_info (@{ $set_info->{problems} }) { + $problem_info->{course_name} = $course_info->{course_name}; + $problem_info->{set_name} = $set_info->{set_name}; + $problem_info->{status} //= 1; + $problem_info->{problem_version} //= 1; + push(@problems_from_json, $problem_info); + } + } + } -my @all_user_problems_from_db = $user_problem_rs->getAllUserProblems(); -for my $user_problem (@all_user_problems_from_db) { - removeIDs($user_problem); - delete $user_problem->{problem_version} unless defined $user_problem->{problem_version}; -} + my @merged_problems_from_json = (); + for my $user_problem (@user_problems_from_json) { + my $problem = clone( + ( + grep { + $_->{course_name} eq $user_problem->{course_name} + && $_->{set_name} eq $user_problem->{set_name} + && $_->{problem_number} == $user_problem->{problem_number} + } @problems_from_json + )[0] + ); -@all_user_problems_from_db = sort user_prob_sort_fxn @all_user_problems_from_db; + # Override the following fields from user problems. + for my $key (qw/seed status problem_version username/) { + $problem->{$key} = $user_problem->{$key} if defined($user_problem->{$key}); + } + # Override any parameters from user problems. + for my $key (keys %{ $user_problem->{problem_params} }) { + $problem->{problem_params}{$key} = $user_problem->{problem_params}{$key} + if defined $problem->{problem_params}{$key}; + } + push(@merged_problems_from_json, $problem); + } -# For comparision, from the database needs to be a number. -$_->{status} = 0 + $_->{status} for (@all_user_problems_from_db); + # FIXME: the getAllUserProblems method should not exist and should not be tested. + # Get all user problems. + my @all_user_problems_from_db = $user_problem_rs->getAllUserProblems(); + for my $user_problem (@all_user_problems_from_db) { + removeIDs($user_problem); + delete $user_problem->{problem_version} unless defined $user_problem->{problem_version}; + } -is(\@all_user_problems_from_db, \@user_problems_from_csv, 'getAllUserProblems: fetch all user problems from the DB.'); + @all_user_problems_from_db = sort user_prob_sort_fcn @all_user_problems_from_db; -# Get merged user problems. + # For comparision, from the database needs to be a number. + $_->{status} = 0 + $_->{status} for (@all_user_problems_from_db); -my @merged_problems_from_db = $user_problem_rs->getAllUserProblems(merged => 1); -for my $merged_problem (@merged_problems_from_db) { - removeIDs($merged_problem); -} + is(\@all_user_problems_from_db, \@user_problems_from_json, + 'getAllUserProblems: fetch all user problems from the DB.'); -# For comparision, from the database needs to be a number. -$_->{status} = 0 + $_->{status} for (@merged_problems_from_db); + # Get merged user problems. + my @merged_problems_from_db = $user_problem_rs->getAllUserProblems(merged => 1); + for my $merged_problem (@merged_problems_from_db) { + removeIDs($merged_problem); + } + + # For comparision, from the database needs to be a number. + $_->{status} = 0 + $_->{status} for (@merged_problems_from_db); + + @merged_problems_from_db = sort user_prob_sort_fcn @merged_problems_from_db; + + # For comparision, from the database needs to be a number. + $_->{status} = 0 + $_->{status} for (@merged_problems_from_db); + + @merged_problems_from_db = sort user_prob_sort_fcn @merged_problems_from_db; -@merged_problems_from_db = sort user_prob_sort_fxn @merged_problems_from_db; + is(\@merged_problems_from_db, \@merged_problems_from_json, 'getAllUserProblems: fetch all merged problems'); -# For comparision, from the database needs to be a number. -$_->{status} = 0 + $_->{status} for (@merged_problems_from_db); + # Get user problems from one course. + my @precalc_user_problems = grep { $_->{course_name} eq 'Precalculus' } @user_problems_from_json; -@merged_problems_from_db = sort user_prob_sort_fxn @merged_problems_from_db; + my @precalc_user_problems_from_db = $user_problem_rs->getUserProblems(info => { course_name => 'Precalculus' }); + for my $user_problem (@precalc_user_problems_from_db) { + removeIDs($user_problem); + delete $user_problem->{problem_version} unless defined $user_problem->{problem_version}; + $user_problem->{status} += 0; + } + + @precalc_user_problems_from_db = sort user_prob_sort_fcn @precalc_user_problems_from_db; + + is(\@precalc_user_problems_from_db, + \@precalc_user_problems, 'getUserProblems: get user problems from a single course.'); -is(\@merged_problems_from_db, \@merged_problems_from_csv, 'getAllUserProblems: fetch all merged problems'); + # Get merged problems from one course. + my @precalc_merged_problems = grep { $_->{course_name} eq 'Precalculus' } @merged_problems_from_json; + + my @precalc_merged_problems_from_db = $user_problem_rs->getUserProblems( + info => { course_name => 'Precalculus' }, + merged => 1 + ); + for my $merged_problem (@precalc_merged_problems_from_db) { + removeIDs($merged_problem); + $merged_problem->{status} += 0; + } -# Get user problems from one course. + @precalc_merged_problems = sort user_prob_sort_fcn @precalc_merged_problems; + @precalc_merged_problems_from_db = sort user_prob_sort_fcn @precalc_merged_problems_from_db; -my @precalc_user_problems = grep { $_->{course_name} eq 'Precalculus' } @user_problems_from_csv; + is(\@precalc_merged_problems_from_db, + \@precalc_merged_problems, 'getUserProblems: get merged problems from a single course.'); -my @precalc_user_problems_from_db = $user_problem_rs->getUserProblems(info => { course_name => 'Precalculus' }); -for my $user_problem (@precalc_user_problems_from_db) { + # Get a single user problem. + my $user_problem = $user_problem_rs->getUserProblem( + info => { course_name => 'Precalculus', username => 'homer', set_name => 'HW #2', problem_number => 3 }); removeIDs($user_problem); delete $user_problem->{problem_version} unless defined $user_problem->{problem_version}; $user_problem->{status} += 0; -} -@precalc_user_problems_from_db = sort user_prob_sort_fxn @precalc_user_problems_from_db; + my $user_problem_from_json = clone( + ( + grep { + $_->{course_name} eq 'Precalculus' + && $_->{username} eq 'homer' + && $_->{set_name} eq 'HW #2' + && $_->{problem_number} == 3 + } @precalc_user_problems + )[0] + ); + + is($user_problem, $user_problem_from_json, 'getUserProblem: get a single user problem'); + + # Get a user problem from a course that doesn't exist. + is( + dies { + $user_problem_rs->getUserProblem( + info => { + course_name => 'course doesn\'t exist', + username => 'homer', + set_name => 'HW #1', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'getUserProblem: attempt to get a user problem from a nonexistent course' + ); + + # Get a user problem from a user that doesn't exist. + is( + dies { + $user_problem_rs->getUserProblem( + info => { + course_name => 'Precalculus', + username => 'non_existent_user', + set_name => 'HW #1', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::UserNotFound'), + 'getUserProblem: attempt to get a user problem from a nonexistent user' + ); + + # Get a user problem from a set that doesn't exist. + is( + dies { + $user_problem_rs->getUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #99', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'getUserProblem: attempt to get a user problem from a nonexistent set' + ); -is(\@precalc_user_problems_from_db, \@precalc_user_problems, - 'getUserProblems: get user problems from a single course.'); + # Get a user problem from a problem that doesn't exist. + is( + dies { + $user_problem_rs->getUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #1', + problem_number => 99 + } + ); + }, + check_isa('DB::Exception::SetProblemNotFound'), + 'getUserProblem: attempt to get a user problem from a nonexistent problem' + ); -# Get merged problems from one course. + # Add a UserProblem to the database. + my $problem_info1 = + { course_name => 'Precalculus', set_name => 'HW #4', username => 'ned', problem_number => 1 }; -my @precalc_merged_problems = grep { $_->{course_name} eq 'Precalculus' } @merged_problems_from_csv; + my $user_problem1 = $user_problem_rs->addUserProblem(params => { %$problem_info1, seed => 7329 }); + removeIDs($user_problem1); + $problem_info1->{seed} = 7329; + $problem_info1->{status} = 0; + $problem_info1->{problem_params} = {}; + $problem_info1->{problem_version} = 1; -my @precalc_merged_problems_from_db = $user_problem_rs->getUserProblems( - info => { course_name => 'Precalculus' }, - merged => 1 -); -for my $merged_problem (@precalc_merged_problems_from_db) { - removeIDs($merged_problem); - $merged_problem->{status} += 0; -} + is($user_problem1, $problem_info1, 'addUserProblem: add a single user problem'); -@precalc_merged_problems = sort user_prob_sort_fxn @precalc_merged_problems; -@precalc_merged_problems_from_db = sort user_prob_sort_fxn @precalc_merged_problems_from_db; + # Add a user problem and get back a merged problem. + my $problem_info2 = + { course_name => 'Precalculus', set_name => 'HW #4', username => 'ned', problem_number => 2 }; -is(\@precalc_merged_problems_from_db, - \@precalc_merged_problems, 'getUserProblems: get merged problems from a single course.'); + my $user_problem2 = $user_problem_rs->addUserProblem( + params => { %$problem_info2, seed => 7329, problem_params => { weight => 2 } }, + merged => 1 + ); -# Get a single user problem. + removeIDs($user_problem2); -my $user_problem = $user_problem_rs->getUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #2', - problem_number => 3 - } -); -removeIDs($user_problem); -delete $user_problem->{problem_version} unless defined $user_problem->{problem_version}; -$user_problem->{status} += 0; - -my $user_problem_from_csv = clone( - ( - grep { - $_->{course_name} eq 'Precalculus' - && $_->{username} eq 'homer' - && $_->{set_name} eq 'HW #2' - && $_->{problem_number} == 3 - } @precalc_user_problems - )[0] -); - -is($user_problem, $user_problem_from_csv, 'getUserProblem: get a single user problem'); - -# Get a user problem from a course that doesn't exist. -is( - dies { - $user_problem_rs->getUserProblem( - info => { - course_name => 'course doesn\'t exist', - username => 'homer', - set_name => 'HW #1', - problem_number => 1 - } - ); - }, - check_isa('DB::Exception::CourseNotFound'), - 'getUserProblem: attempt to get a user problem from a nonexistent course' -); - -# Get a user problem from a user that doesn't exist. -is( - dies { - $user_problem_rs->getUserProblem( - info => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1', - problem_number => 1 - } - ); - }, - check_isa('DB::Exception::UserNotFound'), - 'getUserProblem: attempt to get a user problem from a nonexistent user' -); - -# Get a user problem from a set that doesn't exist. -is( - dies { - $user_problem_rs->getUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #99', - problem_number => 1 - } - ); - }, - check_isa('DB::Exception::SetNotInCourse'), - 'getUserProblem: attempt to get a user problem from a nonexistent set' -); - -# Get a user problem from a problem that doesn't exist. -is( - dies { - $user_problem_rs->getUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 99 - } - ); - }, - check_isa('DB::Exception::SetProblemNotFound'), - 'getUserProblem: attempt to get a user problem from a nonexistent problem' -); - -# Add a UserProblem to the database. - -my $problem_info1 = { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 1 -}; + my $problem2 = clone( + ( + grep { + $_->{course_name} eq $problem_info2->{course_name} + && $_->{set_name} eq $problem_info2->{set_name} + && $_->{problem_number} eq $problem_info2->{problem_number} + } @problems_from_json + )[0] + ); -my $user_problem1 = $user_problem_rs->addUserProblem( - params => { - %$problem_info1, seed => 7329 + # Merge the two problems. + for my $key (qw/username seed status problem_version/) { + $problem2->{$key} = $user_problem2->{$key} if defined $user_problem2->{$key}; } -); -removeIDs($user_problem1); -$problem_info1->{seed} = 7329; -$problem_info1->{status} = 0; -$problem_info1->{problem_params} = {}; -$problem_info1->{problem_version} = 1; - -is($user_problem1, $problem_info1, 'addUserProblem: add a single user problem'); - -# Add a user problem and get back a merged problem. - -my $problem_info2 = { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 -}; + $problem2->{problem_params}{weight} = 2; + + is($user_problem2, $problem2, 'addUserProblem: add a user problem and return a merged problem'); + + # Attempt to add a UserProblem to a non-existent course. + is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'course doesn\'t exist', + username => 'homer', + set_name => 'HW #1', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'addUserProblem: attempt to add a user problem to a nonexistent course' + ); -my $user_problem2 = $user_problem_rs->addUserProblem( - params => { - %$problem_info2, - seed => 7329, - problem_params => { - weight => 2 - } - }, - merged => 1 -); - -removeIDs($user_problem2); - -my $problem2 = clone( - ( - grep { - $_->{course_name} eq $problem_info2->{course_name} - && $_->{set_name} eq $problem_info2->{set_name} - && $_->{problem_number} eq $problem_info2->{problem_number} - } @problems_from_csv - )[0] -); - -# Merge the two problems. -for my $key (qw/username seed status problem_version/) { - $problem2->{$key} = $user_problem2->{$key} if defined $user_problem2->{$key}; -} -$problem2->{problem_params}->{weight} = 2; - -is($user_problem2, $problem2, 'addUserProblem: add a user problem and return a merged problem'); - -# Attempt to add a UserProblem to a non-existent course. -is( - dies { - $user_problem_rs->addUserProblem( - params => { - course_name => 'course doesn\'t exist', - username => 'homer', - set_name => 'HW #1', - problem_number => 1 - } - ); - }, - check_isa('DB::Exception::CourseNotFound'), - 'addUserProblem: attempt to add a user problem to a nonexistent course' -); - -# Attempt to add a UserProblem for a non-existent problem set. -is( - dies { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #999', - problem_number => 1 - } - ); - }, - check_isa('DB::Exception::SetNotInCourse'), - 'addUserProblem: attempt to add a user problem for a non-existent problem set' -); - -# Attempt to add a UserProblem for a non-existent user. -is( - dies { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1', - problem_number => 1 - } - ); - }, - check_isa('DB::Exception::UserNotFound'), - 'addUserProblem: attempt to add a user problem for a non-existent user' -); - -# Attempt to add a UserProblem for a non-existent problem. -is( - dies { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 999 - } - ); - }, - check_isa('DB::Exception::SetProblemNotFound'), - 'addUserProblem: attempt to add a user problem for a non-existent problem' -); - -# Attempt to add a UserProblem that already exists -is( - dies { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 1 - } - ); - }, - check_isa('DB::Exception::UserProblemExists'), - 'addUserProblem: attempt to add a user problem that already exists' -); - -# Try to add a UserProblem with a bad seed :) . -is( - dies { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - seed => -1234 - } - ); - }, - check_isa('DB::Exception::InvalidParameter'), - 'addUserProblem: attempt to add a user problem with a bad seed' -); - -# Attempt to add a new User Problem with a non existent field -is( - dies { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - non_existent_field => 1 - } - ); - }, - check_isa('DBIx::Class::Exception'), - 'addUserProblem: attempt to add a user problem with a non existent field' -); - -# Attempt to add a new User Problem with a non existent field -like( - dies { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - non_existent_field => 1 - } - ); - }, - qr/No such column 'non_existent_field'/, - 'addUserProblem: attempt to add a user problem with a non existent field' -); - -# Attempt to add a new User Problem with invalid library id -like( - dies { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - problem_params => { - library_id => -1234 + # Attempt to add a UserProblem for a non-existent problem set. + is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #999', + problem_number => 1 } - } - ); - }, - qr/library_id is not valid/, - 'addUserProblem: attempt to add a user problem with with invalid library id' -); - -# Attempt to add a new User Problem with a bad problem weight -like( - dies { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - problem_params => { - weight => -1 + ); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'addUserProblem: attempt to add a user problem for a non-existent problem set' + ); + + # Attempt to add a UserProblem for a non-existent user. + is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + username => 'non_existent_user', + set_name => 'HW #1', + problem_number => 1 } - } - ); - }, - qr/weight is not valid/, - 'addUserProblem: attempt to add a user problem with a bad problem weight' -); - -# Attempt to add a new User Problem with a invalid problem_pool_id -like( - dies { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - problem_params => { - problem_pool_id => 'fred' + ); + }, + check_isa('DB::Exception::UserNotFound'), + 'addUserProblem: attempt to add a user problem for a non-existent user' + ); + + # Attempt to add a UserProblem for a non-existent problem. + is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #1', + problem_number => 999 } - } - ); - }, - qr/problem_pool_id is not valid/, - 'addUserProblem: attempt to add a user problem with a bad problem_pool_id' -); + ); + }, + check_isa('DB::Exception::SetProblemNotFound'), + 'addUserProblem: attempt to add a user problem for a non-existent problem' + ); -# update a user problem and return as a user problem + # Attempt to add a UserProblem that already exists + is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #1', + problem_number => 1 + } + ); + }, + check_isa('DB::Exception::UserProblemExists'), + 'addUserProblem: attempt to add a user problem that already exists' + ); -my $updated_problem1 = $user_problem_rs->updateUserProblem( - info => $problem_info1, - params => { - seed => 4567 - } -); -removeIDs($updated_problem1); -# the status needs be returned to a numerical value. -$updated_problem1->{status} += 0; - -delete $updated_problem1->{problem_version} unless defined $updated_problem1->{problem_version}; -$user_problem1->{seed} = 4567; - -is($updated_problem1, $user_problem1, 'updateUserProblem: sucessfully update a field'); - -# Update a user problem and return as a merged problem. - -my $updated_problem2 = $user_problem_rs->updateUserProblem( - info => $problem_info2, - params => { - seed => 4567 - }, - merged => 1 -); -removeIDs($updated_problem2); -$problem2->{seed} = 4567; -$updated_problem2->{status} += 0; - -is($updated_problem2, $problem2, 'updateUserProblem: sucessfully update a field and return as a merged problem'); - -# Update a user problem in the problem_params - -my $updated_problem1a = $user_problem_rs->updateUserProblem( - info => $problem_info1, - params => { - problem_params => { - library_id => 1234 - } - } -); -removeIDs($updated_problem1a); -$updated_problem1a->{status} += 0; - -delete $updated_problem1a->{problem_version} unless defined $updated_problem1a->{problem_version}; -$user_problem1->{problem_params}->{library_id} = 1234; -is($updated_problem1a, $user_problem1, 'updateUserProblem: sucessfully update the problem_params'); - -# Attempt to update a UserProblem to a non-existent course. - -is( - dies { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'course doesn\'t exist', - username => 'homer', - set_name => 'HW #1', - problem_number => 1 - }, - params => {} - ); - }, - check_isa('DB::Exception::CourseNotFound'), - 'updateUserProblem: attempt to update a user problem to a nonexistent course' -); - -# Attempt to update a UserProblem for a non-existent problem set. - -is( - dies { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #999', - problem_number => 1 - }, - params => {} - ); - }, - check_isa('DB::Exception::SetNotInCourse'), - 'updateUserProblem: attempt to update a user problem for a non-existent problem set' -); - -# Attempt to add a UserProblem for a non-existent user. - -is( - dies { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1', - problem_number => 1 - }, - params => {} - ); - }, - check_isa('DB::Exception::UserNotFound'), - 'updateUserProblem: attempt to update a user problem for a non-existent user' -); - -# Attempt to update a UserProblem for a non-existent problem. - -is( - dies { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 999 - }, - params => {} - ); - }, - check_isa('DB::Exception::SetProblemNotFound'), - 'updateUserProblem: attempt to update a user problem for a non-existent problem' -); - -# Try to update a UserProblem with a bad seed :) . - -is( - dies { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - seed => -1234 - } - ); - }, - check_isa('DB::Exception::InvalidParameter'), - 'updateUserProblem: attempt to update a user problem with a bad seed' -); - -# Attempt to update a new User Problem with a non existent field - -is( - dies { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - non_existent_field => 1 - } - ); - }, - check_isa('DBIx::Class::Exception'), - 'updateUserProblem: attempt to update a user problem with a non existent field' -); - -# Attempt to update a new User Problem with a non existent field - -like( - dies { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - non_existent_field => 1 - } - ); - }, - qr/No such column 'non_existent_field'/, - 'updateUserProblem: attempt to update a user problem with a non existent field' -); - -# Attempt to update a new User Problem with invalid library id - -like( - dies { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - problem_params => { - library_id => -1234 + # Try to add a UserProblem with a bad seed :) . + is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + seed => -1234 } - } - ); - }, - qr/library_id is not valid/, - 'updateUserProblem: attempt to update a user problem with with invalid library id' -); - -# Attempt to update a new User Problem with a bad problem weight - -like( - dies { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - problem_params => { - weight => -1 + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'addUserProblem: attempt to add a user problem with a bad seed' + ); + + # Attempt to add a new User Problem with a non existent field + is( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + non_existent_field => 1 } - } - ); - }, - qr/weight is not valid/, - 'updateUserProblem: attempt to update a user problem with a bad problem weight' -); - -# Attempt to add a new User Problem with a invalid problem_pool_id - -like( - dies { - $user_problem_rs->updateUserProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 2 - }, - params => { - problem_params => { - problem_pool_id => 'fred' + ); + }, + check_isa('DBIx::Class::Exception'), + 'addUserProblem: attempt to add a user problem with a non existent field' + ); + + # Attempt to add a new User Problem with a non existent field + like( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + non_existent_field => 1 } - } - ); - }, - qr/problem_pool_id is not valid/, - 'updateUserProblem: attempt to update a user problem with a bad problem_pool_id' -); + ); + }, + qr/No such column 'non_existent_field'/, + 'addUserProblem: attempt to add a user problem with a non existent field' + ); -# Get an array of user problems for a single user in a course. + # Attempt to add a new User Problem with invalid library id + like( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + problem_params => { library_id => -1234 } + } + ); + }, + qr/library_id is not valid/, + 'addUserProblem: attempt to add a user problem with with invalid library id' + ); -my @user_problems = $user_problem_rs->getUserProblemsForUser( - info => { - course_name => 'Precalculus', - username => 'homer' - } -); -for my $user_problem (@user_problems) { - removeIDs($user_problem); - $user_problem->{status} += 0; -} + # Attempt to add a new User Problem with a bad problem weight + like( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + problem_params => { weight => -1 } + } + ); + }, + qr/weight is not valid/, + 'addUserProblem: attempt to add a user problem with a bad problem weight' + ); + + # Attempt to add a new User Problem with a invalid problem_pool_id + like( + dies { + $user_problem_rs->addUserProblem( + params => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 3, + problem_params => { problem_pool_id => 'fred' } + } + ); + }, + qr/problem_pool_id is not valid/, + 'addUserProblem: attempt to add a user problem with a bad problem_pool_id' + ); -my @course_user_problems_from_csv = - grep { $_->{course_name} eq 'Precalculus' && $_->{username} eq 'homer' } @user_problems_from_csv; + # update a user problem and return as a user problem + my $updated_problem1 = $user_problem_rs->updateUserProblem(info => $problem_info1, params => { seed => 4567 }); + removeIDs($updated_problem1); + # the status needs be returned to a numerical value. + $updated_problem1->{status} += 0; -is( - \@user_problems, - \@course_user_problems_from_csv, - 'getCourseUserProblems: get all user problems for a single user in a course' -); + delete $updated_problem1->{problem_version} unless defined $updated_problem1->{problem_version}; + $user_problem1->{seed} = 4567; -# Delete a User Problem + is($updated_problem1, $user_problem1, 'updateUserProblem: sucessfully update a field'); -my $user_problem_to_delete = $user_problem_rs->deleteUserProblem(info => $problem_info1); -removeIDs($user_problem_to_delete); -# the status needs be returned to a numerical value. -$user_problem_to_delete->{status} += 0; + # Update a user problem and return as a merged problem. + my $updated_problem2 = + $user_problem_rs->updateUserProblem(info => $problem_info2, params => { seed => 4567 }, merged => 1); + removeIDs($updated_problem2); + $problem2->{seed} = 4567; + $updated_problem2->{status} += 0; -delete $user_problem_to_delete->{problem_version} unless defined $user_problem_to_delete->{problem_version}; + is($updated_problem2, $problem2, + 'updateUserProblem: sucessfully update a field and return as a merged problem'); -is($user_problem_to_delete, $user_problem1, 'deleteUserProblem: delete a single user problem'); + # Update a user problem in the problem_params + my $updated_problem1a = $user_problem_rs->updateUserProblem( + info => $problem_info1, + params => { problem_params => { library_id => 1234 } } + ); + removeIDs($updated_problem1a); + $updated_problem1a->{status} += 0; + + delete $updated_problem1a->{problem_version} unless defined $updated_problem1a->{problem_version}; + $user_problem1->{problem_params}{library_id} = 1234; + is($updated_problem1a, $user_problem1, 'updateUserProblem: sucessfully update the problem_params'); + + # Attempt to update a UserProblem to a non-existent course. + is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'course doesn\'t exist', + username => 'homer', + set_name => 'HW #1', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'updateUserProblem: attempt to update a user problem to a nonexistent course' + ); -# Delete a user problem and return as a merged problem. + # Attempt to update a UserProblem for a non-existent problem set. + is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #999', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'updateUserProblem: attempt to update a user problem for a non-existent problem set' + ); -my $user_problem_to_delete2 = $user_problem_rs->deleteUserProblem(info => $problem_info2, merged => 1); -removeIDs($user_problem_to_delete2); -# the status needs be returned to a numerical value. -$user_problem_to_delete2->{status} += 0; + # Attempt to add a UserProblem for a non-existent user. + is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + username => 'non_existent_user', + set_name => 'HW #1', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::UserNotFound'), + 'updateUserProblem: attempt to update a user problem for a non-existent user' + ); -is($user_problem_to_delete2, $problem2, 'updateUserProblem: sucessfully update a field and return as a merged problem'); + # Attempt to update a UserProblem for a non-existent problem. + is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #1', + problem_number => 999 + }, + params => {} + ); + }, + check_isa('DB::Exception::SetProblemNotFound'), + 'updateUserProblem: attempt to update a user problem for a non-existent problem' + ); -# Attempt to delete a UserProblem to a non-existent course. + # Try to update a UserProblem with a bad seed :) . + is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { seed => -1234 } + ); + }, + check_isa('DB::Exception::InvalidParameter'), + 'updateUserProblem: attempt to update a user problem with a bad seed' + ); -is( - dies { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'course doesn\'t exist', - username => 'homer', - set_name => 'HW #1', - problem_number => 1 - }, - params => {} - ); - }, - check_isa('DB::Exception::CourseNotFound'), - 'deleteUserProblem: attempt to delete a user problem to a nonexistent course' -); - -# Attempt to delete a UserProblem for a non-existent problem set. - -is( - dies { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #999', - problem_number => 1 - }, - params => {} - ); - }, - check_isa('DB::Exception::SetNotInCourse'), - 'deleteUserProblem: attempt to delete a user problem for a non-existent problem set' -); - -# Attempt to delete a UserProblem for a non-existent user. - -is( - dies { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1', - problem_number => 1 - }, - params => {} - ); - }, - check_isa('DB::Exception::UserNotFound'), - 'deleteUserProblem: attempt to delete a user problem for a non-existent user' -); - -# Attempt to delete a UserProblem for a non-existent problem. - -is( - dies { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 99 - }, - params => {} - ); - }, - check_isa('DB::Exception::SetProblemNotFound'), - 'deleteUserProblem: attempt to delete a user problem for a non-existent problem' -); - -# Ensure that the user_problems table is restored. -@all_user_problems_from_db = $user_problem_rs->getAllUserProblems(); -for my $user_problem (@all_user_problems_from_db) { - removeIDs($user_problem); - delete $user_problem->{problem_version} unless defined $user_problem->{problem_version}; - $user_problem->{status} += 0; -} + # Attempt to update a new User Problem with a non existent field + is( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { non_existent_field => 1 } + ); + }, + check_isa('DBIx::Class::Exception'), + 'updateUserProblem: attempt to update a user problem with a non existent field' + ); -@all_user_problems_from_db = sort user_prob_sort_fxn @all_user_problems_from_db; + # Attempt to update a new User Problem with a non existent field + like( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { non_existent_field => 1 } + ); + }, + qr/No such column 'non_existent_field'/, + 'updateUserProblem: attempt to update a user problem with a non existent field' + ); + + # Attempt to update a new User Problem with invalid library id + like( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { problem_params => { library_id => -1234 } } + ); + }, + qr/library_id is not valid/, + 'updateUserProblem: attempt to update a user problem with with invalid library id' + ); + + # Attempt to update a new User Problem with a bad problem weight + like( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { problem_params => { weight => -1 } } + ); + }, + qr/weight is not valid/, + 'updateUserProblem: attempt to update a user problem with a bad problem weight' + ); + + # Attempt to add a new User Problem with a invalid problem_pool_id + like( + dies { + $user_problem_rs->updateUserProblem( + info => { + course_name => 'Precalculus', + set_name => 'HW #4', + username => 'ned', + problem_number => 2 + }, + params => { problem_params => { problem_pool_id => 'fred' } } + ); + }, + qr/problem_pool_id is not valid/, + 'updateUserProblem: attempt to update a user problem with a bad problem_pool_id' + ); + + # Get an array of user problems for a single user in a course. + my @user_problems = + $user_problem_rs->getUserProblemsForUser(info => { course_name => 'Precalculus', username => 'homer' }); + for my $user_problem (@user_problems) { + removeIDs($user_problem); + $user_problem->{status} += 0; + } + + my @course_user_problems_from_json = + grep { $_->{course_name} eq 'Precalculus' && $_->{username} eq 'homer' } @user_problems_from_json; -is(\@all_user_problems_from_db, \@user_problems_from_csv, 'check: Ensure that the set_problems table is restored.'); + is( + \@user_problems, + \@course_user_problems_from_json, + 'getCourseUserProblems: get all user problems for a single user in a course' + ); + + # Delete a User Problem + my $user_problem_to_delete = $user_problem_rs->deleteUserProblem(info => $problem_info1); + removeIDs($user_problem_to_delete); + # the status needs be returned to a numerical value. + $user_problem_to_delete->{status} += 0; + + delete $user_problem_to_delete->{problem_version} + unless defined $user_problem_to_delete->{problem_version}; + + is($user_problem_to_delete, $user_problem1, 'deleteUserProblem: delete a single user problem'); + + # Delete a user problem and return as a merged problem. + my $user_problem_to_delete2 = $user_problem_rs->deleteUserProblem(info => $problem_info2, merged => 1); + removeIDs($user_problem_to_delete2); + # the status needs be returned to a numerical value. + $user_problem_to_delete2->{status} += 0; + + is($user_problem_to_delete2, $problem2, + 'updateUserProblem: sucessfully update a field and return as a merged problem'); + + # Attempt to delete a UserProblem to a non-existent course. + is( + dies { + $user_problem_rs->deleteUserProblem( + info => { + course_name => 'course doesn\'t exist', + username => 'homer', + set_name => 'HW #1', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::CourseNotFound'), + 'deleteUserProblem: attempt to delete a user problem to a nonexistent course' + ); + + # Attempt to delete a UserProblem for a non-existent problem set. + is( + dies { + $user_problem_rs->deleteUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #999', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::SetNotInCourse'), + 'deleteUserProblem: attempt to delete a user problem for a non-existent problem set' + ); + + # Attempt to delete a UserProblem for a non-existent user. + is( + dies { + $user_problem_rs->deleteUserProblem( + info => { + course_name => 'Precalculus', + username => 'non_existent_user', + set_name => 'HW #1', + problem_number => 1 + }, + params => {} + ); + }, + check_isa('DB::Exception::UserNotFound'), + 'deleteUserProblem: attempt to delete a user problem for a non-existent user' + ); + + # Attempt to delete a UserProblem for a non-existent problem. + is( + dies { + $user_problem_rs->deleteUserProblem( + info => { + course_name => 'Precalculus', + username => 'homer', + set_name => 'HW #1', + problem_number => 99 + }, + params => {} + ); + }, + check_isa('DB::Exception::SetProblemNotFound'), + 'deleteUserProblem: attempt to delete a user problem for a non-existent problem' + ); +}; done_testing; diff --git a/t/db/011_attempts.t b/t/db/011_attempts.t index 05152764..395f11c6 100644 --- a/t/db/011_attempts.t +++ b/t/db/011_attempts.t @@ -2,117 +2,70 @@ # This tests the basic database CRUD functions of attempts. -use warnings; -use strict; +use Test2::V0; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; +use Mojo::File qw/curfile/; -use Test2::V0; -use YAML::XS qw/LoadFile/; +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; -use DB::Schema; +use BuildDB qw/loadPermissions addCourses addUsers addSets addProblems addUserSets addUserProblems/; +use DBSubtest qw/dbSubtest/; use TestUtils qw/removeIDs/; -# Set up the database. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); - -my $config = LoadFile($config_file); - -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); -# $schema->storage->debug(1); # print out the SQL commands. - -my $user_problem_rs = $schema->resultset('UserProblem'); -my $attempt_rs = $schema->resultset('Attempt'); - -# Delete previously added attempts. -# Question: should we instead write a deleteAttempt method and delete at the end of the test? - -my $attempts = $attempt_rs->search( - { - 'courses.course_name' => 'Precalculus', - 'users.username' => 'homer', - 'problem_set.set_name' => 'HW #2' - }, - { - join => { - user_problem => { - user_sets => [ - { - 'course_users' => 'users' - }, - { - 'problem_set' => 'courses' - } - ] - } - } - } -); +my $ww3_dir = curfile->dirname->dirname->dirname; -$attempts->delete_all; +dbSubtest 'course users' => sub ($schema) { + # Add the neccessary sample data to the database. + loadPermissions($schema, $ww3_dir); + addCourses($schema, $ww3_dir); + addUsers($schema, $ww3_dir); + addSets($schema, $ww3_dir); + addProblems($schema, $ww3_dir); + addUserSets($schema, $ww3_dir); + addUserProblems($schema, $ww3_dir); -# Add a few attempts for a give User Problem. + # FIXME: This doesn't need all of the data above. It needs 1 course, 1 set, 1 problem, 1 user, and 1 user problem. -my $user_problem_info = { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #2', - problem_number => 3 -}; + my $user_problem_rs = $schema->resultset('UserProblem'); + my $attempt_rs = $schema->resultset('Attempt'); -my $attempt_params1 = { - scores => [ 0, 1, 1 ], - answers => [ 'x', 'x^2', 'x^3' ], - comments => {} -}; + # Add a few attempts for a given user problem. + my $user_problem_info = + { course_name => 'Precalculus', username => 'homer', set_name => 'HW #2', problem_number => 3 }; -my $attempt1 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params1 }); -removeIDs($attempt1); + my $attempt_params1 = { scores => [ 0, 1, 1 ], answers => [ 'x', 'x^2', 'x^3' ], comments => {} }; -is($attempt1, $attempt_params1, 'addAttempt: add an attempt'); + my $attempt1 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params1 }); + removeIDs($attempt1); -my $attempt_params2 = { - scores => [ 0, 1, 1 ], - answers => [ '2x', '3x^2', '4x^3' ], - comments => {} -}; + is($attempt1, $attempt_params1, 'addAttempt: add an attempt'); -my $attempt2 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params2 }); -removeIDs($attempt2); -is($attempt2, $attempt_params2, 'addAttempt: add another attempt'); + my $attempt_params2 = { scores => [ 0, 1, 1 ], answers => [ '2x', '3x^2', '4x^3' ], comments => {} }; -my $attempt_params3 = { - scores => [ 0, 0, 0 ], - answers => [ '-2x', '2x^2', '4x^3' ], - comments => {} -}; + my $attempt2 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params2 }); + removeIDs($attempt2); + is($attempt2, $attempt_params2, 'addAttempt: add another attempt'); -my $attempt3 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params3 }); -removeIDs($attempt3); -is($attempt3, $attempt_params3, 'addAttempt: add yet another attempt'); + my $attempt_params3 = { scores => [ 0, 0, 0 ], answers => [ '-2x', '2x^2', '4x^3' ], comments => {} }; -my @all_attempts = $attempt_rs->getAttempts(info => $user_problem_info); -for my $attempt (@all_attempts) { - removeIDs($attempt); -} + my $attempt3 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params3 }); + removeIDs($attempt3); + is($attempt3, $attempt_params3, 'addAttempt: add yet another attempt'); -is( - \@all_attempts, - [ $attempt_params1, $attempt_params2, $attempt_params3 ], - "getAttempts: get attempts for a user problem;" -); + my @all_attempts = $attempt_rs->getAttempts(info => $user_problem_info); + for my $attempt (@all_attempts) { + removeIDs($attempt); + } + + is( + \@all_attempts, + [ $attempt_params1, $attempt_params2, $attempt_params3 ], + "getAttempts: get attempts for a user problem;" + ); +}; done_testing; diff --git a/t/db/012_set_versions.t b/t/db/012_set_versions.t index e5fac2cb..3e6f6cc6 100644 --- a/t/db/012_set_versions.t +++ b/t/db/012_set_versions.t @@ -2,227 +2,211 @@ # This tests the basic functions related to versioning in user sets. -use warnings; -use strict; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +use Test2::V0; -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -use Test2::V0; -use YAML::XS qw/LoadFile/; +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json/; use Clone qw/clone/; -use DB::Schema; -use TestUtils qw/loadCSV removeIDs cleanUndef/; - -# Load the database -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -my $problem_set_rs = $schema->resultset('ProblemSet'); -my $course_rs = $schema->resultset('Course'); -my $user_rs = $schema->resultset('User'); -my $user_set_rs = $schema->resultset('UserSet'); - -# Load HW sets from CSV file -my @hw_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/hw_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] - } -); -for my $hw_set (@hw_sets) { - $hw_set->{set_type} = 'HW'; - $hw_set->{set_params} = {} unless defined $hw_set->{set_params}; - -} - -my @quizzes = loadCSV( - "$main::ww3_dir/t/db/sample_data/quizzes.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => ['timed'], - param_non_neg_int_fields => ['quiz_duration'] - } -); -for my $quiz (@quizzes) { - $quiz->{set_type} = "QUIZ"; - $quiz->{set_params} = {} unless defined($quiz->{set_params}); -} - -my @review_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/review_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => ['can_retake'] +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use BuildDB qw/loadPermissions addCourses addUsers addSets addUserSets/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs cleanUndef/; + +my $ww3_dir = curfile->dirname->dirname->dirname; + +dbSubtest 'course users' => sub ($schema) { + # Add the neccessary sample data to the database. + loadPermissions($schema, $ww3_dir); + addCourses($schema, $ww3_dir); + addUsers($schema, $ww3_dir); + addSets($schema, $ww3_dir); + addUserSets($schema, $ww3_dir); + + my $user_set_rs = $schema->resultset('UserSet'); + + # Load HW sets from JSON file. + my $course_hw_sets = decode_json($ww3_dir->child('t/db/sample_data/hw_sets.json')->slurp); + my @hw_sets; + for my $course_data (@$course_hw_sets) { + for my $hw_set (@{ $course_data->{sets} }) { + $hw_set->{set_type} = 'HW'; + $hw_set->{course_name} = $course_data->{course_name}; + push(@hw_sets, $hw_set); + } } -); -for my $set (@review_sets) { - $set->{set_type} = 'REVIEW'; - $set->{set_params} = {} unless defined $set->{set_params}; -} - -my @all_problem_sets = (@hw_sets, @quizzes, @review_sets); - -my @all_user_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/user_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] + # Load quiz sets from JSON file. + my $course_quizzes = decode_json($ww3_dir->child('t/db/sample_data/quizzes.json')->slurp); + my @quizzes; + for my $course_data (@$course_quizzes) { + for my $quiz (@{ $course_data->{sets} }) { + $quiz->{set_type} = 'QUIZ'; + $quiz->{course_name} = $course_data->{course_name}; + push(@quizzes, $quiz); + } } -); - -for my $set (@all_user_sets) { - $set->{set_version} = 0 unless defined($set->{set_version}); - # find the problem set type - my $s = - (grep { $_->{course_name} eq $set->{course_name} && $_->{set_name} eq $set->{set_name} } @all_problem_sets)[0]; - $set->{set_type} = $s->{set_type}; - $set->{set_params} = {} unless defined $set->{set_params}; -} - -my @merged_user_sets = @{ clone(\@all_user_sets) }; - -# Merge the sets -for my $user_set (@merged_user_sets) { - my $set = (grep { $_->{course_name} eq $user_set->{course_name} && $_->{set_name} eq $user_set->{set_name} } - @all_problem_sets)[0]; - - # override problem set dates with userset dates if exist - my $dates = clone($set->{set_dates}); - for my $d (keys %{ $user_set->{set_dates} }) { - $dates->{$d} = $user_set->{set_dates}->{$d}; + # Load review sets from JSON file. + my $course_review_sets = decode_json($ww3_dir->child('t/db/sample_data/review_sets.json')->slurp); + my @review_sets; + for my $course_data (@$course_review_sets) { + for my $review_set (@{ $course_data->{sets} }) { + $review_set->{set_type} = 'REVIEW'; + $review_set->{course_name} = $course_data->{course_name}; + push(@review_sets, $review_set); + } } - # Determine params and dates overrides - my $params = clone($set->{set_params}); - for my $key (keys %{ $user_set->{set_params} }) { - $params->{$key} = $user_set->{set_params}->{$key}; + my @all_problem_sets = (@hw_sets, @quizzes, @review_sets); + + my $course_set_users = decode_json($ww3_dir->child('t/db/sample_data/user_sets.json')->slurp); + my @all_user_sets; + for my $course_data (@$course_set_users) { + for my $set_data (@{ $course_data->{sets} }) { + for my $user_data (@{ $set_data->{users} }) { + $user_data->{user_set}{course_name} = $course_data->{course_name}; + $user_data->{user_set}{set_name} = $set_data->{set_name}; + $user_data->{user_set}{username} = $user_data->{username}; + $user_data->{user_set}{set_version} //= 0; + $user_data->{user_set}{set_dates} //= {}; + $user_data->{user_set}{set_params} //= {}; + $user_data->{user_set}{set_type} = ( + grep { + $_->{course_name} eq $course_data->{course_name} && $_->{set_name} eq $set_data->{set_name} + } @all_problem_sets + )[0]{set_type}; + push(@all_user_sets, $user_data->{user_set}); + } + } } - $user_set->{set_params} = $params; - $user_set->{set_dates} = $dates; - $user_set->{set_version} = 0 unless defined($user_set->{set_version}); - $user_set->{set_type} = $set->{set_type} unless defined($user_set->{set_type}); - $user_set->{set_visible} = $set->{set_visible} unless defined($user_set->{set_visible}); -} - -# Get a user set from a course - -my $user_set_info1 = { - username => 'bart', - course_name => 'Precalculus', - set_name => 'HW #2' -}; + for my $set (@all_user_sets) { + $set->{set_version} = 0 unless defined($set->{set_version}); + # find the problem set type + my $s = + (grep { $_->{course_name} eq $set->{course_name} && $_->{set_name} eq $set->{set_name} } @all_problem_sets) + [0]; + $set->{set_type} = $s->{set_type}; + $set->{set_params} = {} unless defined $set->{set_params}; + } -my $user_set1 = $user_set_rs->getUserSet(info => $user_set_info1); -removeIDs($user_set1); -cleanUndef($user_set1); + my @merged_user_sets = @{ clone(\@all_user_sets) }; -# Check that it is the same as that from the CSV file + # Merge the sets -my $user_set1_from_csv = ( - grep { - $_->{course_name} eq $user_set_info1->{course_name} - && $_->{set_name} eq $user_set_info1->{set_name} - && $_->{username} eq $user_set_info1->{username} - } @all_user_sets -)[0]; + for my $user_set (@merged_user_sets) { + my $set = (grep { $_->{course_name} eq $user_set->{course_name} && $_->{set_name} eq $user_set->{set_name} } + @all_problem_sets)[0]; -# Make a new user set that has a set_version of 1 + # override problem set dates with userset dates if exist + my $dates = clone($set->{set_dates}); + for my $d (keys %{ $user_set->{set_dates} }) { + $dates->{$d} = $user_set->{set_dates}{$d}; + } -is($user_set1, $user_set1_from_csv, 'getUserSet: get a single user set from a course.'); + # Determine params and dates overrides + my $params = clone($set->{set_params}); + for my $key (keys %{ $user_set->{set_params} }) { + $params->{$key} = $user_set->{set_params}{$key}; + } -my $user_set1_v1_params = clone $user_set1; -$user_set1_v1_params->{set_version} = 1; + $user_set->{set_params} = $params; + $user_set->{set_dates} = $dates; + $user_set->{set_version} = 0 unless defined($user_set->{set_version}); + $user_set->{set_type} = $set->{set_type} unless defined($user_set->{set_type}); + $user_set->{set_visible} = $set->{set_visible} unless defined($user_set->{set_visible}); + } -my $user_set1_v1 = $user_set_rs->addUserSet(params => { %$user_set_info1, %$user_set1_v1_params }); -removeIDs($user_set1_v1); -cleanUndef($user_set1_v1); + # Get a user set from a course -is($user_set1_v1, $user_set1_v1_params, "addUserSet: add a user set with version =1 "); + my $user_set_info1 = { + username => 'bart', + course_name => 'Precalculus', + set_name => 'HW #2' + }; -# Make a new user set that has a set_version of 2 + my $user_set1 = $user_set_rs->getUserSet(info => $user_set_info1); + removeIDs($user_set1); + cleanUndef($user_set1); -my $user_set1_v2_params = clone $user_set1_v1_params; -$user_set1_v2_params->{set_version} = 2; + # Check that it is the same as that from the JSON file + my $user_set1_from_json = ( + grep { + $_->{course_name} eq $user_set_info1->{course_name} + && $_->{set_name} eq $user_set_info1->{set_name} + && $_->{username} eq $user_set_info1->{username} + } @all_user_sets + )[0]; -my $user_set1_v2 = $user_set_rs->addUserSet(params => { %$user_set_info1, %$user_set1_v2_params }); -removeIDs($user_set1_v2); -cleanUndef($user_set1_v2); + # Make a new user set that has a set_version of 1 -is($user_set1_v2, $user_set1_v2_params, "addUserSet: add a user set with version = 2."); + is($user_set1, $user_set1_from_json, 'getUserSet: get a single user set from a course.'); -my @all_user_set_versions = $user_set_rs->getUserSetVersions(info => $user_set_info1); -for my $user_set (@all_user_set_versions) { - removeIDs($user_set); - cleanUndef($user_set); -} + my $user_set1_v1_params = clone $user_set1; + $user_set1_v1_params->{set_version} = 1; -is( - \@all_user_set_versions, - [ $user_set1, $user_set1_v1, $user_set1_v2 ], - 'getUserSetVersions: get all versions of a user set.' -); + my $user_set1_v1 = $user_set_rs->addUserSet(params => { %$user_set_info1, %$user_set1_v1_params }); + removeIDs($user_set1_v1); + cleanUndef($user_set1_v1); -# clean up the created versioned user sets. + is($user_set1_v1, $user_set1_v1_params, "addUserSet: add a user set with version =1 "); -my $user_set_v1_to_delete = $user_set_rs->deleteUserSet( - info => { - course_name => $user_set1_v1_params->{course_name}, - set_name => $user_set1_v1_params->{set_name}, - username => $user_set1_v1_params->{username}, - set_version => $user_set1_v1_params->{set_version} - } -); - -removeIDs($user_set_v1_to_delete); -cleanUndef($user_set_v1_to_delete); -is($user_set_v1_to_delete, $user_set1_v1, 'deleteUserSet: delete user set with set_version = 1'); - -my $user_set_v2_to_delete = $user_set_rs->deleteUserSet( - info => { - course_name => $user_set1_v2_params->{course_name}, - set_name => $user_set1_v2_params->{set_name}, - username => $user_set1_v2_params->{username}, - set_version => $user_set1_v2_params->{set_version} - } -); + # Make a new user set that has a set_version of 2 -removeIDs($user_set_v2_to_delete); -cleanUndef($user_set_v2_to_delete); -is($user_set_v2_to_delete, $user_set1_v2, 'deleteUserSet: delete a versioned user set'); + my $user_set1_v2_params = clone $user_set1_v1_params; + $user_set1_v2_params->{set_version} = 2; -# Ensure that the user_sets table is restored. -my @all_user_sets_from_db = $user_set_rs->getAllUserSets(merged => 1); + my $user_set1_v2 = $user_set_rs->addUserSet(params => { %$user_set_info1, %$user_set1_v2_params }); + removeIDs($user_set1_v2); + cleanUndef($user_set1_v2); -for my $set (@all_user_sets_from_db) { - removeIDs($set); - cleanUndef($set); -} + is($user_set1_v2, $user_set1_v2_params, "addUserSet: add a user set with version = 2."); -# Sort before comparing. -@merged_user_sets = - sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @merged_user_sets; -@all_user_sets_from_db = - sort { $a->{course_name} cmp $b->{course_name} || $a->{set_name} cmp $b->{set_name} } @all_user_sets_from_db; + my @all_user_set_versions = $user_set_rs->getUserSetVersions(info => $user_set_info1); + for my $user_set (@all_user_set_versions) { + removeIDs($user_set); + cleanUndef($user_set); + } -is(\@all_user_sets_from_db, \@merged_user_sets, 'check: Ensure that the user_sets table is restored.'); + is( + \@all_user_set_versions, + [ $user_set1, $user_set1_v1, $user_set1_v2 ], + 'getUserSetVersions: get all versions of a user set.' + ); + + # clean up the created versioned user sets. + + my $user_set_v1_to_delete = $user_set_rs->deleteUserSet( + info => { + course_name => $user_set1_v1_params->{course_name}, + set_name => $user_set1_v1_params->{set_name}, + username => $user_set1_v1_params->{username}, + set_version => $user_set1_v1_params->{set_version} + } + ); + + removeIDs($user_set_v1_to_delete); + cleanUndef($user_set_v1_to_delete); + is($user_set_v1_to_delete, $user_set1_v1, 'deleteUserSet: delete user set with set_version = 1'); + + my $user_set_v2_to_delete = $user_set_rs->deleteUserSet( + info => { + course_name => $user_set1_v2_params->{course_name}, + set_name => $user_set1_v2_params->{set_name}, + username => $user_set1_v2_params->{username}, + set_version => $user_set1_v2_params->{set_version} + } + ); + + removeIDs($user_set_v2_to_delete); + cleanUndef($user_set_v2_to_delete); + is($user_set_v2_to_delete, $user_set1_v2, 'deleteUserSet: delete a versioned user set'); +}; done_testing; diff --git a/t/db/013_problem_versions.t b/t/db/013_problem_versions.t index 90101d15..e3c4810d 100644 --- a/t/db/013_problem_versions.t +++ b/t/db/013_problem_versions.t @@ -2,180 +2,191 @@ # This tests the basic functions related to versioning in user sets. -use warnings; -use strict; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +use Test2::V0; -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -use Test2::V0; -use YAML::XS qw/LoadFile/; +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json/; use Clone qw/clone/; -use DB::Schema; -use TestUtils qw/loadCSV removeIDs/; - -# Load the database -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -my $problem_rs = $schema->resultset('SetProblem'); -my $user_problem_rs = $schema->resultset('UserProblem'); - -# Load problems and user problems from the CSV files. -my @user_problems_from_csv = loadCSV("$main::ww3_dir/t/db/sample_data/user_problems.csv"); -for my $user_problem (@user_problems_from_csv) { - $user_problem->{status} = 1 unless defined($user_problem->{status}); - $user_problem->{problem_params} = {} unless defined($user_problem->{problem_params}); - $user_problem->{problem_version} = 1 unless defined($user_problem->{problem_version}); -} - -my @problems_from_csv = loadCSV("$main::ww3_dir/t/db/sample_data/problems.csv"); -for my $problem (@problems_from_csv) { - $problem->{status} = 1 unless defined($problem->{status}); - $problem->{problem_version} = 1 unless defined($problem->{problem_version}); -} - -my @merged_problems_from_csv = (); -for my $user_problem (@user_problems_from_csv) { - my $problem = clone( - ( - grep { - $_->{course_name} eq $user_problem->{course_name} - && $_->{set_name} eq $user_problem->{set_name} - && $_->{problem_number} == $user_problem->{problem_number} - } @problems_from_csv - )[0] - ); - - # Override the following fields from user problems. - for my $key (qw/seed status problem_version username/) { - $problem->{$key} = $user_problem->{$key} if defined($user_problem->{$key}); - } - # Override any parameters from user problems. - for my $key (keys %{ $user_problem->{problem_params} }) { - $problem->{problem_params}->{$key} = $user_problem->{problem_params}->{$key} - if defined $problem->{problem_params}->{$key}; +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use BuildDB qw/loadPermissions addCourses addUsers addSets addProblems addUserSets addUserProblems/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs/; + +my $ww3_dir = curfile->dirname->dirname->dirname; + +dbSubtest 'course users' => sub ($schema) { + # Add the neccessary sample data to the database. + loadPermissions($schema, $ww3_dir); + addCourses($schema, $ww3_dir); + addUsers($schema, $ww3_dir); + addSets($schema, $ww3_dir); + addProblems($schema, $ww3_dir); + addUserSets($schema, $ww3_dir); + addUserProblems($schema, $ww3_dir); + + my $problem_rs = $schema->resultset('SetProblem'); + my $user_problem_rs = $schema->resultset('UserProblem'); + + # Load user problems from the JSON file. + my $course_set_problem_user_problems = + decode_json($ww3_dir->child('t/db/sample_data/user_problems.json')->slurp); + my @user_problems_from_json; + for my $course_info (@$course_set_problem_user_problems) { + for my $set_info (@{ $course_info->{sets} }) { + for my $problem_info (@{ $set_info->{problems} }) { + for my $user_info (@{ $problem_info->{users} }) { + $user_info->{user_problem}{course_name} = $course_info->{course_name}; + $user_info->{user_problem}{set_name} = $set_info->{set_name}; + $user_info->{user_problem}{problem_number} = $problem_info->{problem_number}; + $user_info->{user_problem}{username} = $user_info->{username}; + $user_info->{user_problem}{status} //= 1; + $user_info->{user_problem}{problem_params} //= {}; + $user_info->{user_problem}{problem_version} //= 1; + push(@user_problems_from_json, $user_info->{user_problem}); + } + } + } } - push(@merged_problems_from_csv, $problem); -} -# Get a user problem from a course - -my $user_problem_info = { - username => 'bart', - course_name => 'Precalculus', - set_name => 'HW #1', - problem_number => 1 -}; -my $user_problem1 = $user_problem_rs->getUserProblem(info => $user_problem_info); -removeIDs($user_problem1); -delete $user_problem1->{set_visible} unless defined $user_problem1->{set_visible}; + # Load all problems from the the JSON file. + my $course_set_problems = decode_json($ww3_dir->child('t/db/sample_data/problems.json')->slurp); + my @problems_from_json; + for my $course_info (@$course_set_problems) { + for my $set_info (@{ $course_info->{sets} }) { + for my $problem_info (@{ $set_info->{problems} }) { + $problem_info->{course_name} = $course_info->{course_name}; + $problem_info->{set_name} = $set_info->{set_name}; + $problem_info->{status} //= 1; + $problem_info->{problem_version} //= 1; + push(@problems_from_json, $problem_info); + } + } + } -# Check that it is the same as that from the CSV file + my @merged_problems_from_json = (); + for my $user_problem (@user_problems_from_json) { + my $problem = clone( + ( + grep { + $_->{course_name} eq $user_problem->{course_name} + && $_->{set_name} eq $user_problem->{set_name} + && $_->{problem_number} == $user_problem->{problem_number} + } @problems_from_json + )[0] + ); + + # Override the following fields from user problems. + for my $key (qw/seed status problem_version username/) { + $problem->{$key} = $user_problem->{$key} if defined($user_problem->{$key}); + } + # Override any parameters from user problems. + for my $key (keys %{ $user_problem->{problem_params} }) { + $problem->{problem_params}{$key} = $user_problem->{problem_params}{$key} + if defined $problem->{problem_params}{$key}; + } + push(@merged_problems_from_json, $problem); + } + # Get a user problem from a course -my $user_problem1_from_csv = clone( - ( - grep { - $_->{course_name} eq $user_problem_info->{course_name} - && $_->{set_name} eq $user_problem_info->{set_name} - && $_->{username} eq $user_problem_info->{username} - && $_->{problem_number} == $user_problem_info->{problem_number} - } @user_problems_from_csv - )[0] -); + my $user_problem_info = { + username => 'bart', + course_name => 'Precalculus', + set_name => 'HW #1', + problem_number => 1 + }; -# the status needs be returned to a numerical value. -$user_problem1->{status} += 0; + my $user_problem1 = $user_problem_rs->getUserProblem(info => $user_problem_info); + removeIDs($user_problem1); + delete $user_problem1->{set_visible} unless defined $user_problem1->{set_visible}; -is($user_problem1, $user_problem1_from_csv, 'getUserProblem: get a single user problem from a course.'); + # Check that it is the same as that from the JSON file -# Make a new user problem that has a problem_version of 2 + my $user_problem1_from_json = clone( + ( + grep { + $_->{course_name} eq $user_problem_info->{course_name} + && $_->{set_name} eq $user_problem_info->{set_name} + && $_->{username} eq $user_problem_info->{username} + && $_->{problem_number} == $user_problem_info->{problem_number} + } @user_problems_from_json + )[0] + ); -my $user_problem1_v2_params = clone $user_problem1; -$user_problem1_v2_params->{problem_version} = 2; + # the status needs be returned to a numerical value. + $user_problem1->{status} += 0; -my $user_problem1_v2 = $user_problem_rs->addUserProblem(params => { %$user_problem_info, %$user_problem1_v2_params }); -removeIDs($user_problem1_v2); + is($user_problem1, $user_problem1_from_json, 'getUserProblem: get a single user problem from a course.'); -is($user_problem1_v2, $user_problem1_v2_params, "addUserProblem: add a user problem with version =2 "); + # Make a new user problem that has a problem_version of 2 -# Make a new user set that has a set_version of 3 + my $user_problem1_v2_params = clone $user_problem1; + $user_problem1_v2_params->{problem_version} = 2; -my $user_problem1_v3_params = clone $user_problem1; -$user_problem1_v3_params->{problem_version} = 3; + my $user_problem1_v2 = + $user_problem_rs->addUserProblem(params => { %$user_problem_info, %$user_problem1_v2_params }); + removeIDs($user_problem1_v2); -my $user_problem1_v3 = $user_problem_rs->addUserProblem(params => { %$user_problem_info, %$user_problem1_v3_params }); -removeIDs($user_problem1_v3); + is($user_problem1_v2, $user_problem1_v2_params, "addUserProblem: add a user problem with version =2 "); -is($user_problem1_v3, $user_problem1_v3_params, "addUserProblem: add a user problem with version =3 "); + # Make a new user set that has a set_version of 3 -my @all_user_problem_versions = $user_problem_rs->getUserProblemVersions(info => $user_problem_info); + my $user_problem1_v3_params = clone $user_problem1; + $user_problem1_v3_params->{problem_version} = 3; -for my $user_problem (@all_user_problem_versions) { - removeIDs($user_problem); - $user_problem->{status} += 0; -} + my $user_problem1_v3 = + $user_problem_rs->addUserProblem(params => { %$user_problem_info, %$user_problem1_v3_params }); + removeIDs($user_problem1_v3); -is( - \@all_user_problem_versions, - [ $user_problem1, $user_problem1_v2, $user_problem1_v3 ], - 'getUserProblemVersions: get all versions of a user problem' -); + is($user_problem1_v3, $user_problem1_v3_params, "addUserProblem: add a user problem with version =3 "); -# clean up the created versioned user sets. + my @all_user_problem_versions = $user_problem_rs->getUserProblemVersions(info => $user_problem_info); -my $user_problem_v2_to_delete = $user_problem_rs->deleteUserProblem( - info => { - course_name => $user_problem1_v2_params->{course_name}, - set_name => $user_problem1_v2_params->{set_name}, - username => $user_problem1_v2_params->{username}, - problem_number => $user_problem1_v2_params->{problem_number}, - problem_version => $user_problem1_v2_params->{problem_version} + for my $user_problem (@all_user_problem_versions) { + removeIDs($user_problem); + $user_problem->{status} += 0; } -); -removeIDs($user_problem_v2_to_delete); -$user_problem_v2_to_delete->{status} += 0; - -is($user_problem_v2_to_delete, $user_problem1_v2, 'deleteUserProblem: delete a versioned user problem'); - -my $user_problem_v3_to_delete = $user_problem_rs->deleteUserProblem( - info => { - course_name => $user_problem1_v3_params->{course_name}, - set_name => $user_problem1_v3_params->{set_name}, - username => $user_problem1_v3_params->{username}, - problem_number => $user_problem1_v3_params->{problem_number}, - problem_version => $user_problem1_v3_params->{problem_version} - } -); -removeIDs($user_problem_v3_to_delete); -$user_problem_v3_to_delete->{status} += 0; -is($user_problem_v3_to_delete, $user_problem1_v3, 'deleteUserProblem: delete another versioned user problem'); + is( + \@all_user_problem_versions, + [ $user_problem1, $user_problem1_v2, $user_problem1_v3 ], + 'getUserProblemVersions: get all versions of a user problem' + ); -# Ensure that the user_problems table is restored. -my @all_user_problems_from_db = $user_problem_rs->getAllUserProblems(); + # clean up the created versioned user sets. -for my $user_problem (@all_user_problems_from_db) { - removeIDs($user_problem); - delete $user_problem->{problem_version} unless defined $user_problem->{problem_version}; -} -# For comparision make sure the loaded status are printed to 5 digits. -$_->{status} += 0 for (@all_user_problems_from_db); + my $user_problem_v2_to_delete = $user_problem_rs->deleteUserProblem( + info => { + course_name => $user_problem1_v2_params->{course_name}, + set_name => $user_problem1_v2_params->{set_name}, + username => $user_problem1_v2_params->{username}, + problem_number => $user_problem1_v2_params->{problem_number}, + problem_version => $user_problem1_v2_params->{problem_version} + } + ); + removeIDs($user_problem_v2_to_delete); + $user_problem_v2_to_delete->{status} += 0; + + is($user_problem_v2_to_delete, $user_problem1_v2, 'deleteUserProblem: delete a versioned user problem'); + + my $user_problem_v3_to_delete = $user_problem_rs->deleteUserProblem( + info => { + course_name => $user_problem1_v3_params->{course_name}, + set_name => $user_problem1_v3_params->{set_name}, + username => $user_problem1_v3_params->{username}, + problem_number => $user_problem1_v3_params->{problem_number}, + problem_version => $user_problem1_v3_params->{problem_version} + } + ); + removeIDs($user_problem_v3_to_delete); + $user_problem_v3_to_delete->{status} += 0; -is(\@all_user_problems_from_db, \@user_problems_from_csv, 'check: ensure that user_problems table is restored.'); + is($user_problem_v3_to_delete, $user_problem1_v3, 'deleteUserProblem: delete another versioned user problem'); +}; done_testing; diff --git a/t/db/README.md b/t/db/README.md deleted file mode 100644 index db7a4a41..00000000 --- a/t/db/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# README for the testing db - -This directory contains numerous tests for the database interactions. To run the tests, -`cd` to the top level of the webwork3 directory. - -1. Run `cp conf/webwork3-test.dist.yml conf/webwork3-test.yml`. This makes a copy of a configuration -file that the testing uses. You can look in that file and make any desired changes. - -2. Run `perl t/db/build_db.pl`. This runs a script which restores the database and -fills the database with data from the `t/db/sample_data` directory. - -3. `prove -r t` which runs all tests in the `t` directory. - -## Alternative - -You can also run an individual test script such as `prove -v t/db/003_users.t`. -This produces a verbose (`-v`) version of the tests and lists the output of -each test. - -## Note - -If you get an error, try rerunning steps 2 and 3 above. This rebuilds the database -and reruns all of the tests. - -## To do - -1. Adding new tests to individual files to ensure coverage. -2. Add new test files for new database functionality. diff --git a/t/db/build_db.pl b/t/db/build_db.pl deleted file mode 100755 index 03bd1146..00000000 --- a/t/db/build_db.pl +++ /dev/null @@ -1,352 +0,0 @@ -#!/usr/bin/env perl - -# This file fills a database with sample data from csv files. - -use warnings; -use strict; -use feature 'say'; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - -use Carp; -use Clone qw/clone/; -use DateTime::Format::Strptime; -use YAML::XS qw/LoadFile/; -use Mojo::JSON qw/true false/; - -use DB::Schema; -use DB::Utils qw/updatePermissions/; -use TestUtils qw/loadCSV/; - -my $verbose = 1; - -# Load the configuration for the database settings. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = LoadFile($config_file); - -# Load the Permissions file -my $role_perm_file = "$main::ww3_dir/conf/permissions.yml"; -$role_perm_file = "$main::ww3_dir/conf/permissions.dist.yml" unless -r $role_perm_file; - -# Connect to the database. -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -# $schema->storage->debug(1); # print out the SQL commands. - -say "restoring the database with dbi: $config->{database_dsn}" if $verbose; - -# Create the database based on the schema. -$schema->deploy({ add_drop_table => 1 }); - -# The permissions need to be loaded into the DB before the rest of the script is run. -updatePermissions($config_file, $role_perm_file); - -my $course_rs = $schema->resultset('Course'); -my $user_rs = $schema->resultset('User'); -my $course_user_rs = $schema->resultset('CourseUser'); -my $problem_set_rs = $schema->resultset('ProblemSet'); -my $problem_pool_rs = $schema->resultset('ProblemPool'); -my $set_problem_rs = $schema->resultset('SetProblem'); -my $user_set_rs = $schema->resultset('UserSet'); -my $role_rs = $schema->resultset('Role'); - -my $strp_date = DateTime::Format::Strptime->new(pattern => '%F', on_error => 'croak'); - -sub addCourses { - say "adding courses" if $verbose; - my @courses = loadCSV( - "$main::ww3_dir/t/db/sample_data/courses.csv", - { - boolean_fields => ['visible'] - } - ); - # currently course_params from the csv file are written to the course_settings database table. - for my $course (@courses) { - $course->{course_settings} = {}; - for my $key (keys %{ $course->{course_params} }) { - my @fields = split(/:/, $key); - $course->{course_settings}->{ $fields[0] } = { $fields[1] => $course->{course_params}->{$key} }; - } - - delete $course->{course_params}; - $course_rs->create($course); - } - return; -} - -sub addUsers { - # Add some users - say 'adding users' if $verbose; - - my @all_students = loadCSV( - "$main::ww3_dir/t/db/sample_data/students.csv", - { - boolean_fields => ['is_admin'] - } - ); - - # Add an admin user - my $admin = { - username => 'admin', - email => 'admin@google.com', - first_name => "Andrea", - last_name => "Administrator", - is_admin => true, - login_params => { password => "admin" } - }; - $user_rs->create($admin); - - for my $student (@all_students) { - my $student_role = $role_rs->find({ role_name => 'student' }); - my $course = $course_rs->find({ course_name => $student->{course_name} }); - my $stud_info = {}; - for my $key (qw/username first_name last_name email student_id/) { - $stud_info->{$key} = $student->{$key}; - } - $stud_info->{login_params} = { password => $student->{username} }; - $course->add_to_users($stud_info, { role_id => $student_role->role_id }); - - my $user = $user_rs->find({ username => $student->{username} }); - my $params = { - user_id => $user->user_id, - course_id => $course->course_id, - }; - - # Look up the role of the user - my $role = $role_rs->find({ role_name => $student->{role} }); - die "The user with username $student->{username} has role $student->{role} which does not exist\n" - . 'Either reassign the role or ensure that bin/update_perms.pl has been run.' - unless defined $role; - - my $course_user = $course_user_rs->find($params); - for my $key (qw/section recitation params/) { - $params->{$key} = $student->{$key}; - } - $params->{role_id} = $role->role_id; - $params->{course_user_params} = $params->{params} // {}; - delete $params->{params}; - my $u = $course_user->update($params); - } - return; -} - -sub addSets { - # Add some problem sets - say 'adding problem sets' if $verbose; - - my @hw_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/hw_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] - } - ); - - for my $set (@hw_sets) { - my $course = $course_rs->find({ course_name => $set->{course_name} }); - if (!defined($course)) { - croak 'The course ' . $set->{course_name} . ' does not exist'; - } - - delete $set->{course_name}; - $course->add_to_problem_sets($set); - } - - # Add quizzes - say 'adding quizzes' if $verbose; - - my @quizzes = loadCSV( - "$main::ww3_dir/t/db/sample_data/quizzes.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => ['timed'], - param_non_neg_int_fields => ['quiz_duration'] - } - ); - for my $quiz (@quizzes) { - my $course = $course_rs->search({ course_name => $quiz->{course_name} })->single; - if (!defined($course)) { - croak 'The course ' . $quiz->{course_name} . ' does not exist'; - } - - $quiz->{type} = 2; - delete $quiz->{course_name}; - - $course->add_to_problem_sets($quiz); - } - - say 'adding review sets' if $verbose; - - my @review_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/review_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => ['can_retake'] - } - ); - for my $set (@review_sets) { - my $course = $course_rs->find({ course_name => $set->{course_name} }); - croak "The course |$set->{course_name}| does not exist" unless defined($course); - - $set->{type} = 4; - delete $set->{course_name}; - $course->add_to_problem_sets($set); - } - - return; -} - -sub addProblems { - # Add some problems - say "adding problems" if $verbose; - my @problems = loadCSV( - "$main::ww3_dir/t/db/sample_data/problems.csv", - { - non_neg_float_fields => ['status'], - non_neg_int_fields => [ 'seed', 'problem_number' ], - param_non_neg_int_fields => ['library_id'], - param_non_neg_float_fields => ['weight'] - } - ); - for my $prob (@problems) { - # Check if the course_name/set_name exists - my $set = $problem_set_rs->find( - { - 'me.set_name' => $prob->{set_name}, - 'courses.course_name' => $prob->{course_name} - }, - { - join => 'courses' - } - ); - croak "The course |$set->{course_name}| with set name |$set->{name}| is not defined" unless defined($set); - delete $prob->{course_name}; - delete $prob->{set_name}; - delete $prob->{params}; - - $prob->{problem_number} = int($prob->{problem_number}); - - my $problem_set = $problem_set_rs->search({ set_id => $set->set_id })->single; - - $problem_set->add_to_problems($prob); - } - return; -} - -sub addUserSets { - # Add some users to problem sets - say "adding user sets" if $verbose; - my @user_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/user_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] - } - ); - for my $user_set (@user_sets) { - # Check if the course_name/set_name/user_name exists - my $course = $course_rs->find({ course_name => $user_set->{course_name} }); - my $user_in_course = $course->users->find({ username => $user_set->{username} }); - my $course_user = $course_user_rs->find({ - user_id => $user_in_course->user_id, - course_id => $course->course_id - }); - # say 'adding the user set for ' . $user_set->{username} . ' in ' . $user_set->{course_name}; - if (defined $course_user) { - my $problem_set = $schema->resultset('ProblemSet')->find({ - course_id => $course->course_id, - set_name => $user_set->{set_name} - }); - for my $key (qw/course_name set_name type username/) { - delete $user_set->{$key}; - } - $user_set->{course_user_id} = $course_user->course_user_id; - $user_set->{set_id} = $problem_set->set_id; - $problem_set->add_to_user_sets($user_set); - } - } - return; -} - -sub addProblemPools { - say 'adding problem pools' if $verbose; - my @problem_pools = my @problem_pools_from_file = - loadCSV("$main::ww3_dir/t/db/sample_data/pool_problems.csv", { non_neg_int_fields => ['library_id'] }); - - for my $pool (@problem_pools) { - my $course = $course_rs->find({ course_name => $pool->{course_name} }); - croak "The course |$pool->{course_name}| does not exist" unless defined($course); - - my $prob_pool = - $problem_pool_rs->find_or_create({ course_id => $course->course_id, pool_name => $pool->{pool_name} }); - $prob_pool->add_to_pool_problems({ params => { library_id => $pool->{params}->{library_id} } }); - - } - return; -} - -sub addUserProblems { - say "adding user problems" if $verbose; - my @user_problems = loadCSV( - "$main::ww3_dir/t/db/sample_data/user_problems.csv", - { - non_neg_float_fields => ['status'], - non_neg_int_fields => [ 'seed', 'problem_number' ], - param_non_neg_int_fields => ['library_id'], - param_non_neg_float_fields => ['weight'] - } - ); - for my $user_problem (@user_problems) { - my $user_set = $user_set_rs->find( - { - 'users.username' => $user_problem->{username}, - 'courses.course_name' => $user_problem->{course_name}, - 'problem_set.set_name' => $user_problem->{set_name} - }, - { - join => [ { problem_set => 'courses' }, { course_users => 'users' } ] - } - ); - my $problem = $set_problem_rs->find( - { - 'courses.course_name' => $user_problem->{course_name}, - 'problem_set.set_name' => $user_problem->{set_name}, - 'problem_number' => $user_problem->{problem_number} - }, - { - join => { 'problem_set' => 'courses' } - } - ); - - $user_set->add_to_user_problems({ - set_problem_id => $problem->set_problem_id, - seed => $user_problem->{seed}, - problem_version => 1, - status => 0 + $user_problem->{status} - }); - } - return; -} - -addCourses; -addUsers; -addSets; -addProblems; -addUserSets; -addProblemPools; -addUserProblems; - -1; diff --git a/t/db/run_all_tests.pl b/t/db/run_all_tests.pl deleted file mode 100755 index 6c352476..00000000 --- a/t/db/run_all_tests.pl +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env perl - -use warnings; -use strict; - -# Run all tests in this directory. - -use TAP::Harness; -use File::Basename qw/dirname/; - -my $test_dir = dirname(__FILE__); - -`perl $test_dir/build_db.pl` unless -e 'sample_db.sqlite'; - -my @test_files = glob("$test_dir/*.t"); - -my %args = (verbosity => 0, lib => [ '.', ]); -my $harness = TAP::Harness->new(\%args); - -$harness->runtests(@test_files); - -1; diff --git a/t/db/sample_data/courses.csv b/t/db/sample_data/courses.csv deleted file mode 100644 index 17c0eb20..00000000 --- a/t/db/sample_data/courses.csv +++ /dev/null @@ -1,6 +0,0 @@ -course_name,visible,COURSE_PARAMS:general:institution,COURSE_DATES:start,COURSE_DATES:end -Precalculus,1,"Springfield CC",2021-01-01,2021-12-31 -"Abstract Algebra",1,"Springfield University",2021-01-01,2021-12-31 -"Topology",1,"Springfield University",2021-01-01,2021-12-31 -Arithmetic,1,"Springfield CC",2020-09-01,2020-12-16 -Calculus,1,"Springfield University",2020-09-01,2020-12-16 diff --git a/t/db/sample_data/courses.json b/t/db/sample_data/courses.json new file mode 100644 index 00000000..3a6165ba --- /dev/null +++ b/t/db/sample_data/courses.json @@ -0,0 +1,32 @@ +[ + { + "course_name": "Precalculus", + "visible": true, + "course_dates": { "start": 1609459200, "end": 1640908800 }, + "course_settings": { "general": { "institution": "Springfield CC" } } + }, + { + "course_name": "Abstract Algebra", + "visible": true, + "course_dates": { "start": 1609459200, "end": 1640908800 }, + "course_settings": { "general": { "institution": "Springfield University" } } + }, + { + "course_name": "Topology", + "visible": true, + "course_dates": { "start": 1609459200, "end": 1640908800 }, + "course_settings": { "general": { "institution": "Springfield University" } } + }, + { + "course_name": "Arithmetic", + "visible": true, + "course_dates": { "start": 1598918400, "end": 1608076800 }, + "course_settings": { "general": { "institution": "Springfield CC" } } + }, + { + "course_name": "Calculus", + "visible": true, + "course_dates": { "start": 1598918400, "end": 1608076800 }, + "course_settings": { "general": { "institution": "Springfield University" } } + } +] diff --git a/t/db/sample_data/hw_sets.csv b/t/db/sample_data/hw_sets.csv deleted file mode 100644 index 032984cf..00000000 --- a/t/db/sample_data/hw_sets.csv +++ /dev/null @@ -1,13 +0,0 @@ -course_name,set_name,set_visible,SET_DATES:open,SET_DATES:reduced_scoring,SET_DATES:due,SET_DATES:answer,SET_DATES:enable_reduced_scoring -Precalculus,"HW #1",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 -Precalculus,"HW #2",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 -Precalculus,"HW #3",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 -Precalculus,"HW #4",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 -Precalculus,"HW #5",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 -Precalculus,"HW #6",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 -"Abstract Algebra","HW #1",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 -"Abstract Algebra","HW #2",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 -"Abstract Algebra","HW #3",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 -Arithmetic,"HW #1",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 -Arithmetic,"HW #2",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 -Arithmetic,"HW #3",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,2021-02-21T23:59:00Z,1 diff --git a/t/db/sample_data/hw_sets.json b/t/db/sample_data/hw_sets.json new file mode 100644 index 00000000..f0b8c4c3 --- /dev/null +++ b/t/db/sample_data/hw_sets.json @@ -0,0 +1,161 @@ +[ + { + "course_name": "Precalculus", + "sets": [ + { + "set_name": "HW #1", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + }, + { + "set_name": "HW #2", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + }, + { + "set_name": "HW #3", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + }, + { + "set_name": "HW #4", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + }, + { + "set_name": "HW #5", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + }, + { + "set_name": "HW #6", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + } + ] + }, + { + "course_name": "Abstract Algebra", + "sets": [ + { + "set_name": "HW #1", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + }, + { + "set_name": "HW #2", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + }, + { + "set_name": "HW #3", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + } + ] + }, + { + "course_name": "Arithmetic", + "sets": [ + { + "set_name": "HW #1", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + }, + { + "set_name": "HW #2", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + }, + { + "set_name": "HW #3", + "set_visible": true, + "set_dates": { + "open": 1609545540, + "reduced_scoring": 1610323140, + "due": 1612137540, + "answer": 1613951940, + "enable_reduced_scoring": true + }, + "set_params": {} + } + ] + } +] diff --git a/t/db/sample_data/pool_problems.csv b/t/db/sample_data/pool_problems.csv deleted file mode 100644 index f4c310fe..00000000 --- a/t/db/sample_data/pool_problems.csv +++ /dev/null @@ -1,19 +0,0 @@ -course_name,pool_name,PARAMS:library_id -Precalculus,"factoring problems",123 -Precalculus,"factoring problems",223 -Precalculus,"factoring problems",133 -Precalculus,"factoring problems",124 -Precalculus,"solve exponentials",1237 -Precalculus,"solve exponentials",2237 -Precalculus,"solve exponentials",1337 -Precalculus,"solve exponentials",1247 -Arithmetic,"adding fractions",4321 -Arithmetic,"adding fractions",9321 -Arithmetic,"adding fractions",431 -Arithmetic,"multiplying fractions",3923 -Arithmetic,"multiplying fractions",3234 -Arithmetic,"multiplying fractions",8372 -Arithmetic,"multiplying fractions",932 -Arithmetic,"mixed numbers",4322 -Arithmetic,"mixed numbers",920 -Arithmetic,"mixed numbers",3219 diff --git a/t/db/sample_data/pool_problems.json b/t/db/sample_data/pool_problems.json new file mode 100644 index 00000000..06dbd85f --- /dev/null +++ b/t/db/sample_data/pool_problems.json @@ -0,0 +1,55 @@ +[ + { + "course_name": "Precalculus", + "pools": [ + { + "pool_name": "factoring problems", + "pool_problems": [ + { "params": { "library_id": 123 } }, + { "params": { "library_id": 223 } }, + { "params": { "library_id": 133 } }, + { "params": { "library_id": 124 } } + ] + }, + { + "pool_name": "solve exponentials", + "pool_problems": [ + { "params": { "library_id": 1237 } }, + { "params": { "library_id": 2237 } }, + { "params": { "library_id": 1337 } }, + { "params": { "library_id": 1247 } } + ] + } + ] + }, + { + "course_name": "Arithmetic", + "pools": [ + { + "pool_name": "adding fractions", + "pool_problems": [ + { "params": { "library_id": 4321 } }, + { "params": { "library_id": 9321 } }, + { "params": { "library_id": 431 } } + ] + }, + { + "pool_name": "mixed numbers", + "pool_problems": [ + { "params": { "library_id": 4322 } }, + { "params": { "library_id": 920 } }, + { "params": { "library_id": 3219 } } + ] + }, + { + "pool_name": "multiplying fractions", + "pool_problems": [ + { "params": { "library_id": 3923 } }, + { "params": { "library_id": 3234 } }, + { "params": { "library_id": 8372 } }, + { "params": { "library_id": 932 } } + ] + } + ] + } +] diff --git a/t/db/sample_data/problems.csv b/t/db/sample_data/problems.csv deleted file mode 100644 index 40efa90b..00000000 --- a/t/db/sample_data/problems.csv +++ /dev/null @@ -1,18 +0,0 @@ -course_name,set_name,problem_number,PROBLEM_PARAMS:library_id,PROBLEM_PARAMS:weight,PROBLEM_PARAMS:file_path -Precalculus,"HW #1",1,1,1 -Precalculus,"HW #1",2,2,1 -Precalculus,"HW #1",3,3,1 -Precalculus,"HW #2",1,4,1 -Precalculus,"HW #2",2,5,1 -Precalculus,"HW #2",3,6,1 -Precalculus,"HW #4",1,4,1 -Precalculus,"HW #4",2,5,1 -Precalculus,"HW #4",3,6,1 -"Abstract Algebra","HW #1",1,10,1 -"Abstract Algebra","HW #1",2,11,1 -"Abstract Algebra","HW #1",3,12,1 -"Abstract Algebra","HW #1",4,13,1 -"Abstract Algebra","HW #1",5,14,1 -Arithmetic,"HW #1",1,,1,"Library/UVA-FinancialMath/setFinancialMath-Sect10-AlgebraPrereqs/math114-0-01.pg" -Arithmetic,"HW #1",2,,2,"Library/PCC/BasicAlgebra/Trigonometry/RightTriangleTrigApplication90.pg" -Arithmetic,"HW #1",3,,3,"Library/PCC/BasicAlgebra/SignedNumbersArithemtic/AdditionWithNegativeNumbers10.pg" diff --git a/t/db/sample_data/problems.json b/t/db/sample_data/problems.json new file mode 100644 index 00000000..a9afbdc5 --- /dev/null +++ b/t/db/sample_data/problems.json @@ -0,0 +1,80 @@ +[ + { + "course_name": "Precalculus", + "sets": [ + { + "set_name": "HW #1", + "problems": [ + { "problem_number": 1, "problem_params": { "library_id": 1, "weight": 1 } }, + { "problem_number": 2, "problem_params": { "library_id": 2, "weight": 1 } }, + { "problem_number": 3, "problem_params": { "library_id": 3, "weight": 1 } } + ] + }, + { + "set_name": "HW #2", + "problems": [ + { "problem_number": 1, "problem_params": { "library_id": 4, "weight": 1 } }, + { "problem_number": 2, "problem_params": { "library_id": 5, "weight": 1 } }, + { "problem_number": 3, "problem_params": { "library_id": 6, "weight": 1 } } + ] + }, + { + "set_name": "HW #4", + "problems": [ + { "problem_number": 1, "problem_params": { "library_id": 4, "weight": 1 } }, + { "problem_number": 2, "problem_params": { "library_id": 5, "weight": 1 } }, + { "problem_number": 3, "problem_params": { "library_id": 6, "weight": 1 } } + ] + } + ] + }, + { + "course_name": "Abstract Algebra", + "sets": [ + { + "set_name": "HW #1", + "problems": [ + { "problem_number": 1, "problem_params": { "library_id": 10, "weight": 1 } }, + { "problem_number": 2, "problem_params": { "library_id": 11, "weight": 1 } }, + { "problem_number": 3, "problem_params": { "library_id": 12, "weight": 1 } }, + { "problem_number": 4, "problem_params": { "library_id": 13, "weight": 1 } }, + { "problem_number": 5, "problem_params": { "library_id": 14, "weight": 1 } } + ] + } + ] + }, + { + "course_name": "Arithmetic", + "sets": [ + { + "set_name": "HW #1", + "problems": [ + { + "problem_number": 1, + "problem_params": { + "file_path": + "Library/UVA-FinancialMath/setFinancialMath-Sect10-AlgebraPrereqs/math114-0-01.pg", + "weight": 1 + } + }, + { + "problem_number": 2, + "problem_params": { + "file_path": + "Library/PCC/BasicAlgebra/Trigonometry/RightTriangleTrigApplication90.pg", + "weight": 2 + } + }, + { + "problem_number": 3, + "problem_params": { + "file_path": + "Library/PCC/BasicAlgebra/SignedNumbersArithemtic/AdditionWithNegativeNumbers10.pg", + "weight": 3 + } + } + ] + } + ] + } +] diff --git a/t/db/sample_data/quizzes.csv b/t/db/sample_data/quizzes.csv deleted file mode 100644 index 2f818313..00000000 --- a/t/db/sample_data/quizzes.csv +++ /dev/null @@ -1,7 +0,0 @@ -course_name,set_name,set_visible,SET_DATES:open,SET_DATES:due,SET_DATES:answer,SET_PARAMS:timed,SET_PARAMS:quiz_duration -Precalculus,"Quiz #1",0,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,1,30 -Precalculus,"Quiz #2",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,1,15 -Precalculus,"Quiz #3",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,0,0 -"Abstract Algebra","Quiz #1",0,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,1,30 -Arithmetic,"Quiz #1",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,1,15 -Arithmetic,"Quiz #2",0,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,2021-01-31T23:59:00Z,0,0 diff --git a/t/db/sample_data/quizzes.json b/t/db/sample_data/quizzes.json new file mode 100644 index 00000000..4136ded9 --- /dev/null +++ b/t/db/sample_data/quizzes.json @@ -0,0 +1,53 @@ +[ + { + "course_name": "Precalculus", + "sets": [ + { + "set_name": "Quiz #1", + "set_visible": false, + "set_dates": { "open": 1609545540, "due": 1610323140, "answer": 1612137540 }, + "set_params": { "quiz_duration": 30, "timed": true } + }, + { + "set_name": "Quiz #2", + "set_visible": true, + "set_dates": { "open": 1609545540, "due": 1610323140, "answer": 1612137540 }, + "set_params": { "quiz_duration": 15, "timed": true } + }, + { + "set_name": "Quiz #3", + "set_visible": true, + "set_dates": { "open": 1609545540, "due": 1610323140, "answer": 1612137540 }, + "set_params": { "quiz_duration": 0, "timed": false } + } + ] + }, + { + "course_name": "Abstract Algebra", + "sets": [ + { + "set_name": "Quiz #1", + "set_visible": false, + "set_dates": { "open": 1609545540, "due": 1610323140, "answer": 1612137540 }, + "set_params": { "quiz_duration": 30, "timed": true } + } + ] + }, + { + "course_name": "Arithmetic", + "sets": [ + { + "set_name": "Quiz #1", + "set_visible": true, + "set_dates": { "open": 1609545540, "due": 1610323140, "answer": 1612137540 }, + "set_params": { "quiz_duration": 15, "timed": true } + }, + { + "set_name": "Quiz #2", + "set_visible": false, + "set_dates": { "open": 1609545540, "due": 1610323140, "answer": 1612137540 }, + "set_params": { "quiz_duration": 0, "timed": false } + } + ] + } +] diff --git a/t/db/sample_data/review_sets.csv b/t/db/sample_data/review_sets.csv deleted file mode 100644 index 6a64db08..00000000 --- a/t/db/sample_data/review_sets.csv +++ /dev/null @@ -1,6 +0,0 @@ -course_name,set_name,set_visible,SET_DATES:open,SET_DATES:closed,SET_PARAMS:can_retake -Precalculus,"Review #1",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,1 -Precalculus,"Review #2",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,1 -Precalculus,"Review #3",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,0 -Arithmetic,"Review #1",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,0 -Arithmetic,"Review #2",1,2021-01-01T23:59:00Z,2021-01-10T23:59:00Z,0 diff --git a/t/db/sample_data/review_sets.json b/t/db/sample_data/review_sets.json new file mode 100644 index 00000000..73f6718d --- /dev/null +++ b/t/db/sample_data/review_sets.json @@ -0,0 +1,42 @@ +[ + { + "course_name": "Precalculus", + "sets": [ + { + "set_name": "Review #1", + "set_visible": true, + "set_dates": { "open": 1609545540, "closed": 1610323140 }, + "set_params": { "can_retake": true } + }, + { + "set_name": "Review #2", + "set_visible": true, + "set_dates": { "open": 1609545540, "closed": 1610323140 }, + "set_params": { "can_retake": true } + }, + { + "set_name": "Review #3", + "set_visible": true, + "set_dates": { "open": 1609545540, "closed": 1610323140 }, + "set_params": { "can_retake": false } + } + ] + }, + { + "course_name": "Arithmetic", + "sets": [ + { + "set_name": "Review #1", + "set_visible": true, + "set_dates": { "open": 1609545540, "closed": 1610323140 }, + "set_params": { "can_retake": false } + }, + { + "set_name": "Review #2", + "set_visible": true, + "set_dates": { "open": 1609545540, "closed": 1610323140 }, + "set_params": { "can_retake": false } + } + ] + } +] diff --git a/t/db/sample_data/students.csv b/t/db/sample_data/students.csv deleted file mode 100644 index 2e622819..00000000 --- a/t/db/sample_data/students.csv +++ /dev/null @@ -1,16 +0,0 @@ -first_name,last_name,email,username,course_name,student_id,recitation,section,PARAMS:comment,PARAMS:useMathQuill,role -Homer,Simpson,homer@aol.com,homer,Precalculus,12,1,,"hi there",1,student -Lisa,Simpson,lisa@google.com,lisa,"Abstract Algebra",23,,,,,student -Lisa,Simpson,lisa@google.com,lisa,"Topology",23,,,,,student -Lisa,Simpson,lisa@google.com,lisa,Arithmetic,23,,,,,instructor -Bart,Simpson,bart@aol.com,bart,Precalculus,132,,,,,student -Marge,Simpson,marge@juno.com,marge,Calculus,234,,,,,student -Barney,Gumble,barney@google.com,barney,Precalculus,492,,,,,student -Moe,Szyslak,moe@msn.com,moe,Arithmetic,023,,,,,student -Ned,Flanders,ned@msn.com,ned,Precalculus,0983,,,,,student -Apu,Nahasapeemapetilon,apu@google.com,apu,Abstract Algebra,8939,,,,,student -Waylon,Smithers,smithers@google.com,smithers,Calculus,3298,,,,,student -Ralph,Wiggum,ralph@ralph.com,ralph,Arithmetic,038,,,,,student -Ralph,Wiggum,ralph@ralph.com,ralph,Precalculus,038,,,,,student -Otto,Mann,otto@msn.com,otto,Precalculus,284,,,,,student -Jonathan,Frink,frink@frink.com,frink,Precalculus,,,,,,instructor diff --git a/t/db/sample_data/user_problems.csv b/t/db/sample_data/user_problems.csv deleted file mode 100644 index b67fa290..00000000 --- a/t/db/sample_data/user_problems.csv +++ /dev/null @@ -1,25 +0,0 @@ -course_name,set_name,problem_number,username,seed,status -Precalculus,"HW #1",1,homer,1234,0 -Precalculus,"HW #1",1,bart,4324,0 -Precalculus,"HW #1",1,ned,324,0 -Precalculus,"HW #1",2,homer,937,0 -Precalculus,"HW #1",2,bart,916,1 -Precalculus,"HW #1",2,ned,583,1 -Precalculus,"HW #1",3,homer,123,0 -Precalculus,"HW #1",3,bart,949,0 -Precalculus,"HW #1",3,ned,562,0.5 -Precalculus,"HW #2",1,homer,239,0 -Precalculus,"HW #2",1,bart,1394,0 -Precalculus,"HW #2",1,ned,321,0 -Precalculus,"HW #2",2,homer,798,1 -Precalculus,"HW #2",2,bart,204,0 -Precalculus,"HW #2",2,ned,908,0 -Precalculus,"HW #2",3,homer,765,1 -Precalculus,"HW #2",3,bart,324,0 -Precalculus,"HW #2",3,ned,821,1 -Arithmetic,"HW #1",1,moe,242,0 -Arithmetic,"HW #1",1,ralph,294,1 -Arithmetic,"HW #1",2,moe,972,0 -Arithmetic,"HW #1",2,ralph,843,0.5 -Arithmetic,"HW #1",3,moe,204,0.5 -Arithmetic,"HW #1",3,ralph,666,0.75 diff --git a/t/db/sample_data/user_problems.json b/t/db/sample_data/user_problems.json new file mode 100644 index 00000000..4754095a --- /dev/null +++ b/t/db/sample_data/user_problems.json @@ -0,0 +1,96 @@ +[ + { + "course_name": "Precalculus", + "sets": [ + { + "set_name": "HW #1", + "problems": [ + { + "problem_number": 1, + "users": [ + { "username": "homer", "user_problem": { "seed": 1234, "status": 0 } }, + { "username": "bart", "user_problem": { "seed": 4324, "status": 0 } }, + { "username": "ned", "user_problem": { "seed": 324, "status": 0 } } + ] + }, + { + "problem_number": 2, + "users": [ + { "username": "homer", "user_problem": { "seed": 937, "status": 0 } }, + { "username": "bart", "user_problem": { "seed": 916, "status": 1 } }, + { "username": "ned", "user_problem": { "seed": 583, "status": 1 } } + ] + }, + { + "problem_number": 3, + "users": [ + { "username": "homer", "user_problem": { "seed": 123, "status": 0 } }, + { "username": "bart", "user_problem": { "seed": 949, "status": 0 } }, + { "username": "ned", "user_problem": { "seed": 562, "status": 0.5 } } + ] + } + ] + }, + { + "set_name": "HW #2", + "problems": [ + { + "problem_number": 1, + "users": [ + { "username": "homer", "user_problem": { "seed": 239, "status": 0 } }, + { "username": "bart", "user_problem": { "seed": 1394, "status": 0 } }, + { "username": "ned", "user_problem": { "seed": 321, "status": 0 } } + ] + }, + { + "problem_number": 2, + "users": [ + { "username": "homer", "user_problem": { "seed": 798, "status": 1 } }, + { "username": "bart", "user_problem": { "seed": 240, "status": 0 } }, + { "username": "ned", "user_problem": { "seed": 908, "status": 0 } } + ] + }, + { + "problem_number": 3, + "users": [ + { "username": "homer", "user_problem": { "seed": 765, "status": 1 } }, + { "username": "bart", "user_problem": { "seed": 324, "status": 0 } }, + { "username": "ned", "user_problem": { "seed": 821, "status": 1 } } + ] + } + ] + } + ] + }, + { + "course_name": "Arithmetic", + "sets": [ + { + "set_name": "HW #1", + "problems": [ + { + "problem_number": 1, + "users": [ + { "username": "moe", "user_problem": { "seed": 242, "status": 0 } }, + { "username": "ralph", "user_problem": { "seed": 294, "status": 1 } } + ] + }, + { + "problem_number": 2, + "users": [ + { "username": "moe", "user_problem": { "seed": 972, "status": 0 } }, + { "username": "ralph", "user_problem": { "seed": 843, "status": 0.5 } } + ] + }, + { + "problem_number": 3, + "users": [ + { "username": "moe", "user_problem": { "seed": 240, "status": 0.5 } }, + { "username": "ralph", "user_problem": { "seed": 666, "status": 0.75 } } + ] + } + ] + } + ] + } +] diff --git a/t/db/sample_data/user_sets.csv b/t/db/sample_data/user_sets.csv deleted file mode 100644 index c1fcb19f..00000000 --- a/t/db/sample_data/user_sets.csv +++ /dev/null @@ -1,17 +0,0 @@ -course_name,username,set_name,SET_DATES:open,SET_DATES:reduced_scoring,SET_DATES:due,SET_DATES:answer,SET_DATES:enable_reduced_scoring -Precalculus,homer,"HW #1",2021-01-08T23:59:00Z,,2021-02-15T23:59:00Z,2021-02-22T23:59:00Z,1 -Precalculus,bart,"HW #1",,,, -Precalculus,ned,"HW #1",,,, -Precalculus,homer,"HW #2",2021-01-15T23:59:00Z,,2021-02-22T23:59:00Z,2021-02-24T23:59:00Z,0 -Precalculus,bart,"HW #2",,,, -Precalculus,ned,"HW #2",,,, -Precalculus,homer,"HW #3",2021-01-15T23:59:00Z,2021-02-20T23:59:00Z,,2021-03-13T23:59:00Z,0 -Precalculus,bart,"HW #3",,,, -Precalculus,ned,"HW #3",,,, -Precalculus,ned,"HW #4",,,, -Arithmetic,moe,"HW #1",,,, -Arithmetic,ralph,"HW #1",,,, -Arithmetic,ralph,"HW #2",,,, -"Abstract Algebra",lisa,"HW #1",,,, -"Abstract Algebra",lisa,"HW #2",,,, -"Abstract Algebra",lisa,"HW #3",,,, diff --git a/t/db/sample_data/user_sets.json b/t/db/sample_data/user_sets.json new file mode 100644 index 00000000..83aaf64e --- /dev/null +++ b/t/db/sample_data/user_sets.json @@ -0,0 +1,86 @@ +[ + { + "course_name": "Precalculus", + "sets": [ + { + "set_name": "HW #1", + "users": [ + { + "username": "homer", + "user_set": { + "set_dates": { + "open": 1610150340, + "due": 1613433540, + "answer": 1614038340, + "enable_reduced_scoring": true + } + } + }, + { "username": "bart", "user_set": {} }, + { "username": "ned", "user_set": {} } + ] + }, + { + "set_name": "HW #2", + "users": [ + { + "username": "homer", + "user_set": { + "set_dates": { + "open": 1610755140, + "due": 1614038340, + "answer": 1614211140, + "enable_reduced_scoring": false + } + } + }, + { "username": "bart", "user_set": {} }, + { "username": "ned", "user_set": {} } + ] + }, + { + "set_name": "HW #3", + "users": [ + { + "username": "homer", + "user_set": { + "set_dates": { + "open": 1610755140, + "reduced_scoring": 1613865540, + "answer": 1615679940, + "enable_reduced_scoring": false + } + } + }, + { "username": "bart", "user_set": {} }, + { "username": "ned", "user_set": {} } + ] + }, + { "set_name": "HW #4", "users": [{ "username": "ned", "user_set": {} }] } + ] + }, + { + "course_name": "Arithmetic", + "sets": [ + { + "set_name": "HW #1", + "users": [ + { "username": "moe", "user_set": {} }, + { "username": "ralph", "user_set": {} } + ] + }, + { + "set_name": "HW #2", + "users": [{ "username": "ralph", "user_set": {} }] + } + ] + }, + { + "course_name": "Abstract Algebra", + "sets": [ + { "set_name": "HW #1", "users": [{ "username": "lisa", "user_set": {} }] }, + { "set_name": "HW #2", "users": [{ "username": "lisa", "user_set": {} }] }, + { "set_name": "HW #3", "users": [{ "username": "lisa", "user_set": {} }] } + ] + } +] diff --git a/t/db/sample_data/users.json b/t/db/sample_data/users.json new file mode 100644 index 00000000..1ee9e483 --- /dev/null +++ b/t/db/sample_data/users.json @@ -0,0 +1,120 @@ +[ + { + "first_name": "Andrea", + "last_name": "Administrator", + "username": "admin", + "email": "admin@google.com", + "is_admin": true + }, + { + "first_name": "Lisa", + "last_name": "Simpson", + "username": "lisa", + "email": "lisa@google.com", + "student_id": "23", + "courses": [ + { "course_name": "Abstract Algebra", "course_user": { "role": "student" } }, + { "course_name": "Arithmetic", "course_user": { "role": "instructor" } }, + { "course_name": "Topology", "course_user": { "role": "student" } } + ] + }, + { + "first_name": "Apu", + "last_name": "Nahasapeemapetilon", + "username": "apu", + "email": "apu@google.com", + "student_id": "8939", + "courses": [{ "course_name": "Abstract Algebra", "course_user": { "role": "student" } }] + }, + { + "first_name": "Moe", + "last_name": "Szyslak", + "username": "moe", + "email": "moe@msn.com", + "student_id": "023", + "courses": [{ "course_name": "Arithmetic", "course_user": { "role": "student" } }] + }, + { + "first_name": "Ralph", + "last_name": "Wiggum", + "username": "ralph", + "email": "ralph@ralph.com", + "student_id": "038", + "courses": [ + { "course_name": "Arithmetic", "course_user": { "role": "student" } }, + { "course_name": "Precalculus", "course_user": { "role": "student" } } + ] + }, + { + "first_name": "Marge", + "last_name": "Simpson", + "username": "marge", + "email": "marge@juno.com", + "student_id": "234", + "courses": [{ "course_name": "Calculus", "course_user": { "role": "student" } }] + }, + { + "first_name": "Waylon", + "last_name": "Smithers", + "username": "smithers", + "email": "smithers@google.com", + "student_id": "3298", + "courses": [{ "course_name": "Calculus", "course_user": { "role": "student" } }] + }, + { + "first_name": "Homer", + "last_name": "Simpson", + "username": "homer", + "email": "homer@aol.com", + "student_id": "12", + "courses": [ + { + "course_name": "Precalculus", + "course_user": { + "role": "student", + "recitation": "1", + "course_user_params": { "comment": "hi there", "usemathquill": true } + } + } + ] + }, + { + "first_name": "Bart", + "last_name": "Simpson", + "username": "bart", + "email": "bart@aol.com", + "student_id": "132", + "courses": [{ "course_name": "Precalculus", "course_user": { "role": "student" } }] + }, + { + "first_name": "Barney", + "last_name": "Gumble", + "username": "barney", + "email": "barney@google.com", + "student_id": "492", + "courses": [{ "course_name": "Precalculus", "course_user": { "role": "student" } }] + }, + { + "first_name": "Ned", + "last_name": "Flanders", + "username": "ned", + "email": "ned@msn.com", + "student_id": "0983", + "courses": [{ "course_name": "Precalculus", "course_user": { "role": "student" } }] + }, + { + "first_name": "Otto", + "last_name": "Mann", + "username": "otto", + "email": "otto@msn.com", + "student_id": "284", + "courses": [{ "course_name": "Precalculus", "course_user": { "role": "student" } }] + }, + { + "first_name": "John", + "last_name": "Frink", + "username": "frink", + "email": "frink@frink.com", + "courses": [{ "course_name": "Precalculus", "course_user": { "role": "instructor" } }] + } +] diff --git a/t/lib/BuildDB.pm b/t/lib/BuildDB.pm new file mode 100644 index 00000000..2e676ddd --- /dev/null +++ b/t/lib/BuildDB.pm @@ -0,0 +1,222 @@ +package BuildDB; +use parent Exporter; + +# This package provides methods for deploying the webwork3 database, and filling it with sample data. + +use Mojo::Base -signatures; +use Carp; +use Mojo::JSON qw/decode_json/; + +use DB::Utils qw/updatePermissions/; + +our @EXPORT_OK = + qw/loadPermissions addCourses addUsers addSets addProblems addUserSets addProblemPools addUserProblems/; + +sub loadPermissions ($schema, $ww3_dir) { + updatePermissions($schema, $ww3_dir->child('conf/permissions.dist.yml')); + return; +} + +sub addCourses ($schema, $ww3_dir) { + my $course_rs = $schema->resultset('Course'); + my $courses = decode_json($ww3_dir->child('t/db/sample_data/courses.json')->slurp); + $course_rs->create($_) for @$courses; + return; +} + +# Note that loadPermissions and addCourses must be called before calling addUsers. +sub addUsers ($schema, $ww3_dir) { + my $course_rs = $schema->resultset('Course'); + my $role_rs = $schema->resultset('Role'); + + my $users = decode_json($ww3_dir->child('t/db/sample_data/users.json')->slurp); + + for my $user (@$users) { + my $courses = delete $user->{courses}; + if (defined $courses) { + for my $course_data (@{$courses}) { + my $course = $course_rs->find({ course_name => $course_data->{course_name} }); + croak qq{The course "$course_data->{course_name}" does not exist.} unless defined $course; + + my $course_user = $course_data->{course_user}; + + # Look up the role of the user + my $role = $role_rs->find({ role_name => $course_user->{role} }); + croak qq{The role "$course_user->{role}" for user "$user->{username}" does not exist.} + unless defined $role; + + delete $course_user->{role}; + $course_user->{role_id} = $role->role_id; + $user->{login_params} = { password => $user->{username} }; + + $course->add_to_users($user, $course_user); + } + } else { + $user->{login_params} = { password => $user->{username} }; + $schema->resultset('User')->create($user); + } + } + return; +} + +# Note that addCourses must be called before calling addSets. +sub addSets ($schema, $ww3_dir) { + my $course_rs = $schema->resultset('Course'); + + # Add homework sets + my $course_hw_sets = decode_json($ww3_dir->child('t/db/sample_data/hw_sets.json')->slurp); + for my $course_data (@$course_hw_sets) { + my $course = $course_rs->find({ course_name => $course_data->{course_name} }); + croak qq{The course "$course_data->{course_name}" does not exist.} unless defined $course; + for (@{ $course_data->{sets} }) { + $course->add_to_problem_sets($_); + } + } + + # Add quizzes + my $course_quizzes = decode_json($ww3_dir->child('t/db/sample_data/quizzes.json')->slurp); + for my $course_data (@$course_quizzes) { + my $course = $course_rs->find({ course_name => $course_data->{course_name} }); + croak qq{The course "$course_data->{course_name}" does not exist.} unless defined $course; + for (@{ $course_data->{sets} }) { + $_->{type} = 2; + $course->add_to_problem_sets($_); + } + } + + # Add review sets + my $course_review_sets = decode_json($ww3_dir->child('t/db/sample_data/review_sets.json')->slurp); + for my $course_data (@$course_review_sets) { + my $course = $course_rs->find({ course_name => $course_data->{course_name} }); + croak qq{The course "$course_data->{course_name}" does not exist.} unless defined $course; + for (@{ $course_data->{sets} }) { + $_->{type} = 4; + $course->add_to_problem_sets($_); + } + } + + return; +} + +# Note that addCourses and addSets must be called before calling addProblems. +sub addProblems ($schema, $ww3_dir) { + my $problem_set_rs = $schema->resultset('ProblemSet'); + + my $course_set_problems = decode_json($ww3_dir->child('t/db/sample_data/problems.json')->slurp); + for my $course_data (@$course_set_problems) { + for my $set_data (@{ $course_data->{sets} }) { + # Check if a course with course_name exists with the set set_name. + my $set = + $problem_set_rs->find( + { 'me.set_name' => $set_data->{set_name}, 'courses.course_name' => $course_data->{course_name} }, + { join => 'courses' }); + croak qq{The course "$course_data->{course_name}" with set "$set_data->{set_name}" does not exist.} + unless defined $set; + + for (@{ $set_data->{problems} }) { + $set->add_to_problems($_); + } + } + } + return; +} + +# Note that all of the previous methods must be called before calling addUserSets. +sub addUserSets ($schema, $ww3_dir) { + my $course_rs = $schema->resultset('Course'); + my $course_user_rs = $schema->resultset('CourseUser'); + + my $course_set_users = decode_json($ww3_dir->child('t/db/sample_data/user_sets.json')->slurp); + + for my $course_data (@$course_set_users) { + # Check if the course exists. + my $course = $course_rs->find({ course_name => $course_data->{course_name} }); + croak qq{The course "$course_data->{course_name}" does not exist.} unless defined $course; + + for my $set_data (@{ $course_data->{sets} }) { + # Check if the set exists. + my $set = $schema->resultset('ProblemSet') + ->find({ course_id => $course->course_id, set_name => $set_data->{set_name} }); + croak qq{The set "$set_data->{set_name}" does not exist.} unless defined $set; + + for my $user_data (@{ $set_data->{users} }) { + # Check if the user exists and is in the course. + my $user = $course->users->find({ username => $user_data->{username} }); + croak qq{The user "$user_data->{username}" does not exist.} unless defined $user; + + my $course_user = $user->course_users->find({ course_id => $course->course_id }); + croak qq{The course user "$user_data->{username}" does not exist } + . qq{in the course "$course_data->{course_name}".} + unless defined $course_user; + + $user_data->{user_set}{course_user_id} = $course_user->course_user_id; + $set->add_to_user_sets($user_data->{user_set}); + } + } + } + return; +} + +# Note that addCourses must be called before calling addProblemPools. +sub addProblemPools ($schema, $ww3_dir) { + my $course_rs = $schema->resultset('Course'); + my $problem_pool_rs = $schema->resultset('ProblemPool'); + + my $course_pool_problems = decode_json($ww3_dir->child('t/db/sample_data/pool_problems.json')->slurp); + + for my $course_info (@$course_pool_problems) { + my $course = $course_rs->find({ course_name => $course_info->{course_name} }); + croak qq{The course "$course_info->{course_name}" does not exist.} unless defined $course; + + for my $problem_pool_info (@{ $course_info->{pools} }) { + my $problem_pool = + $problem_pool_rs->create( + { course_id => $course->course_id, pool_name => $problem_pool_info->{pool_name} }); + for (@{ $problem_pool_info->{pool_problems} }) { + $problem_pool->add_to_pool_problems($_); + } + } + } + return; +} + +# Note that all of the previous methods except addProblemPools must be called before calling addUserProblems. +sub addUserProblems ($schema, $ww3_dir) { + my $user_set_rs = $schema->resultset('UserSet'); + my $set_problem_rs = $schema->resultset('SetProblem'); + + my $course_set_problem_user_problems = decode_json($ww3_dir->child('t/db/sample_data/user_problems.json')->slurp); + + for my $course_info (@$course_set_problem_user_problems) { + for my $set_info (@{ $course_info->{sets} }) { + for my $problem_info (@{ $set_info->{problems} }) { + my $problem = $set_problem_rs->find( + { + 'courses.course_name' => $course_info->{course_name}, + 'problem_set.set_name' => $set_info->{set_name}, + 'problem_number' => $problem_info->{problem_number} + }, + { join => { 'problem_set' => 'courses' } } + ); + + for my $user_info (@{ $problem_info->{users} }) { + $user_set_rs->find( + { + 'users.username' => $user_info->{username}, + 'courses.course_name' => $course_info->{course_name}, + 'problem_set.set_name' => $set_info->{set_name} + }, + { join => [ { problem_set => 'courses' }, { course_users => 'users' } ] } + )->add_to_user_problems({ + set_problem_id => $problem->set_problem_id, + %{ $user_info->{user_problem} } + }); + + } + } + } + } + return; +} + +1; diff --git a/t/lib/DBSubtest.pm b/t/lib/DBSubtest.pm new file mode 100644 index 00000000..d8bebe2d --- /dev/null +++ b/t/lib/DBSubtest.pm @@ -0,0 +1,104 @@ +package DBSubtest; +use parent Exporter; + +use Mojo::Base -signatures; +use Test2::Tools qw/plan skip_all/; +use Test2::Tools::AsyncSubtest; +use Test::PostgreSQL; + +use DB::Schema; +use TestMysqld; + +our @EXPORT_OK = qw/dbSubtest mojoDBSubtest/; + +sub dbSubtest ($name, $code) { + my @db_subtests; + + for my $db_type ($ENV{WW3_TEST_ALL_DBS} ? ('sqlite', 'postgres', 'mysql') : 'sqlite') { + push( + @db_subtests, + fork_subtest "Test $name with database $db_type" => sub { + my ($sqld, $schema); + + # Load the database. + if ($db_type eq 'postgres') { + $sqld = eval { Test::PostgreSQL->new } + or plan(skip_all => 'Unable to initialize psql instance'); + $schema = DB::Schema->connect($sqld->dsn, undef, undef, + { quote_names => 1, on_connect_do => 'SET client_min_messages=WARNING;' }); + } elsif ($db_type eq 'mysql') { + $sqld = eval { TestMysqld->new(my_cnf => { 'skip-networking' => '' }) } + or plan(skip_all => $TestMysqld::errstr); + $schema = DB::Schema->connect($sqld->dsn); + } else { + $schema = DB::Schema->connect('dbi:SQLite:dbname=:memory:'); + } + + # Deploy the database. + $schema->deploy({ add_drop_table => 1 }); + + # Execute the subtest. + $code->($schema); + } + ); + } + + (shift @db_subtests)->finish while (@db_subtests); + + return; +} + +sub mojoDBSubtest ($name, $code) { + my @db_subtests; + + for my $db_type ($ENV{WW3_TEST_ALL_DBS} ? ('sqlite', 'postgres', 'mysql') : 'sqlite') { + push( + @db_subtests, + fork_subtest "Test $name with database $db_type" => sub { + my ($sqld, $dsn); + + # Load the database. + if ($db_type eq 'postgres') { + $sqld = eval { Test::PostgreSQL->new } + or plan(skip_all => 'Unable to initialize psql instance'); + $dsn = $sqld->dsn; + } elsif ($db_type eq 'mysql') { + $sqld = eval { TestMysqld->new(my_cnf => { 'skip-networking' => '' }) } + or plan(skip_all => $TestMysqld::errstr); + $dsn = $sqld->dsn; + } else { + $dsn = 'dbi:SQLite:dbname=:memory:'; + } + + my $t = Test2::MojoX->new( + WeBWorK3 => { + secrets => ['1234'], + database_dsn => $dsn, + cookie_secure => 0, + cookie_lifetime => 3600, + $db_type eq 'postgres' ? (database_on_connect_do => 'SET client_min_messages=WARNING;') : () + } + ); + + my $schema = $t->app->schema; + + # Deploy the database. + $schema->deploy({ add_drop_table => 1 }); + + # Execute the subtest. + $code->($t, $schema); + + # This must be done here for postgres or exceptions are thrown after the test finishes in some cases + # because the postgres daemon can stop before the Mojolicious app disconnects the schema from the + # database. It doesn't hurt for the others. + $schema->storage->disconnect; + } + ); + } + + (shift @db_subtests)->finish while (@db_subtests); + + return; +} + +1; diff --git a/t/lib/TestMysqld.pm b/t/lib/TestMysqld.pm new file mode 100644 index 00000000..c98a420b --- /dev/null +++ b/t/lib/TestMysqld.pm @@ -0,0 +1,270 @@ +package TestMysqld; + +=head1 TestMysqld + +This is a slightly simplified version of the Test::mysqld package. See +L for usage. However the +C option, the C and C methods, and +all POD have been removed. In addition the code has been rewritten to not +depend on the Class::Accessor::Lite and File::Copy::Recursive packages. + +=cut + +use strict; +use warnings; +use feature 'signatures'; +no warnings qw/experimental::signatures/; + +use Cwd; +use DBI; +use File::Temp qw(tempdir); +use POSIX qw(SIGTERM WNOHANG); +use Time::HiRes qw(sleep); + +our $errstr; +our @SEARCH_PATHS = qw(/usr/local/mysql); + +sub new ($klass, @options) { + my $self = bless { + auto_start => 2, + base_dir => undef, + my_cnf => {}, + mysqld => undef, + use_mysqld_initialize => undef, + mysql_install_db => undef, + pid => undef, + _owner_pid => undef, + @options == 1 ? %{ $options[0] } : @options, + _owner_pid => $$ + }, $klass; + + if (defined $self->{base_dir}) { + $self->{base_dir} = cwd . '/' . $self->{base_dir} if $self->{base_dir} !~ m|^/|; + } else { + $self->{base_dir} = tempdir(CLEANUP => $ENV{TEST_MYSQLD_PRESERVE} ? undef : 1); + } + + $self->{my_cnf}{socket} ||= "$self->{base_dir}/tmp/mysql.sock"; + $self->{my_cnf}{datadir} ||= "$self->{base_dir}/var"; + $self->{my_cnf}{pid_file} ||= "$self->{base_dir}/tmp/mysqld.pid"; + $self->{my_cnf}{tmpdir} ||= "$self->{base_dir}/tmp"; + + if (!defined $self->{mysqld}) { + my $prog = _find_program('mysqld', qw/bin libexec sbin/) or die 'unable to find mysqld program'; + $self->{mysqld} = $prog; + } + if (!defined $self->{use_mysqld_initialize}) { + $self->{use_mysqld_initialize} = $self->_use_mysqld_initialize; + } + + if ($self->{auto_start}) { + die 'mysqld is already running (' . $self->{my_cnf}{pid_file} . ')' + if -e $self->{my_cnf}{pid_file}; + + $self->setup if $self->{auto_start} >= 2; + $self->start; + } + + return $self; +} + +sub DESTROY ($self) { + $self->stop if defined $self->{pid} && $$ == $self->{_owner_pid}; + return; +} + +sub dsn ($self, %args) { + $args{port} ||= $self->{my_cnf}{port} if $self->{my_cnf}{port}; + if (defined $args{port}) { + $args{host} ||= $self->{my_cnf}{host} || '127.0.0.1'; + } else { + $args{mysql_socket} ||= $self->{my_cnf}{socket}; + } + $args{user} = $self->{my_cnf}{user} if $self->{my_cnf}{user}; + $args{dbname} = $self->{my_cnf}{dbname} // 'test'; + return 'DBI:mysql:' . join(';', map {"$_=$args{$_}"} sort keys %args); +} + +sub start ($self) { + return if defined $self->{pid}; + $self->spawn; + $self->wait_for_setup; + return; +} + +sub spawn ($self) { + return if defined $self->{pid}; + + ## no critic (InputOutput::RequireBriefOpen) + open my $logfh, '>>', "$self->{base_dir}/tmp/mysqld.log" + or die "failed to create log file: $self->{base_dir}/tmp/mysqld.log:$!"; + my $pid = fork; + die "fork(2) failed:$!" unless defined $pid; + if ($pid == 0) { + open STDOUT, '>&', $logfh or die "dup(2) failed:$!"; + open STDERR, '>&', $logfh or die "dup(2) failed:$!"; + if ($self->{my_cnf}{user} eq 'root') { + exec($self->{mysqld}, "--defaults-file=$self->{base_dir}/etc/my.cnf", '--user=root'); + } else { + exec($self->{mysqld}, "--defaults-file=$self->{base_dir}/etc/my.cnf"); + } + die "failed to launch mysqld:$?"; + } + close $logfh; + ## use critic (InputOutput::RequireBriefOpen) + $self->{pid} = $pid; + + return; +} + +sub wait_for_setup ($self) { + return unless defined $self->{pid}; + my $pid = $self->{pid}; + while (!-e $self->{my_cnf}{pid_file}) { + if (waitpid($pid, WNOHANG) > 0) { + die "*** failed to launch mysqld ***\n" . $self->read_log; + } + sleep 0.1; + } + + # create 'test' database + my $dbh = DBI->connect($self->dsn) or die $DBI::errstr; + $dbh->do('CREATE DATABASE IF NOT EXISTS ' . ($self->{my_cnf}{dbname} // 'test')) or die $dbh->errstr; + + return; +} + +sub stop ($self, $sig = SIGTERM) { + return unless defined $self->{pid}; + $self->send_stop_signal($sig); + $self->wait_for_stop; + return; +} + +sub send_stop_signal ($self, $sig = SIGTERM) { + return unless defined $self->{pid}; + kill $sig, $self->{pid}; + return; +} + +sub wait_for_stop ($self) { + local $?; # waitpid may change this value :/ + while (waitpid($self->{pid}, 0) <= 0) { } + $self->{pid} = undef; + # might remain for example when sending SIGKILL + unlink $self->{my_cnf}{pid_file}; + return; +} + +sub setup ($self) { + # (re)create directory structure + mkdir $self->{base_dir}; + for my $subdir (qw/etc var tmp/) { + mkdir "$self->{base_dir}/$subdir"; + } + + # my.cnf + open my $fh, '>', "$self->{base_dir}/etc/my.cnf" + or die "failed to create file: $self->{base_dir}/etc/my.cnf:$!"; + print $fh "[mysqld]\n"; + print $fh map { defined $self->{my_cnf}{$_} && length $self->{my_cnf}{$_} ? "$_=$self->{my_cnf}{$_}\n" : "$_\n"; } + sort keys %{ $self->{my_cnf} }; + close $fh; + + # mysql_install_db + if (!-d "$self->{base_dir}/var/mysql") { + my $cmd = $self->{use_mysqld_initialize} ? $self->{mysqld} : do { + if (!defined $self->{mysql_install_db}) { + my $prog = _find_program('mysql_install_db', qw/bin scripts/) + or die 'failed to find mysql_install_db'; + $self->{mysql_install_db} = $prog; + } + $self->{mysql_install_db}; + }; + + # We should specify --defaults-file option first. + $cmd .= " --defaults-file='$self->{base_dir}/etc/my.cnf'"; + + if ($self->{use_mysqld_initialize}) { + $cmd .= ' --initialize-insecure'; + } else { + # `abs_path` resolves nested symlinks and returns canonical absolute path + my $mysql_base_dir = Cwd::abs_path($self->{mysql_install_db}); + if ($mysql_base_dir =~ s{/(?:bin|extra|scripts)/mysql_install_db$}{}) { + $cmd .= " --basedir='$mysql_base_dir'"; + } + } + $cmd .= ' 2>&1'; + + # The MySQL scripts are in Perl, so clear out all current Perl related environment variables before the call. + local @ENV{ grep {/^PERL/} keys %ENV }; + + ## no critic (InputOutput::RequireBriefOpen) + my $output; + open $fh, '-|', $cmd or die "failed to spawn mysql_install_db:$!"; + while (my $l = <$fh>) { $output .= $l; } + close $fh or die "*** mysql_install_db failed ***\n% $cmd\n$output\n"; + ## use critic (InputOutput::RequireBriefOpen) + } + + return; +} + +sub read_log ($self) { + open my $logfh, '<', "$self->{base_dir}/tmp/mysqld.log" or die "failed to open file:tmp/mysql.log:$!"; + my $log_contents = do { local $/; <$logfh> }; + close $logfh; + return $log_contents; +} + +sub _find_program ($prog, @subdirs) { + undef $errstr; + my $path = _get_path_of($prog); + return $path if $path; + for my $mysql (_get_path_of('mysql'), map {"$_/bin/mysql"} @SEARCH_PATHS) { + if (-x $mysql) { + for my $subdir (@subdirs) { + $path = $mysql; + return $path if ($path =~ s|/bin/mysql$|/$subdir/$prog| && -x $path); + } + } + } + $errstr = "could not find $prog, please set appropriate PATH"; + return; +} + +sub _verbose_help ($self) { + return $self->{_verbose_help} ||= `$self->{mysqld} --verbose --help 2>/dev/null`; +} + +# Detect if mysqld supports `--initialize-insecure` option or not from the output of `mysqld --help --verbose`. +# `mysql_install_db` command is obsoleted for MySQL 5.7.6 or later and `mysqld --initialize-insecure` should be used. +sub _use_mysqld_initialize ($self) { + return $self->_verbose_help =~ /--initialize-insecure/ms; +} + +sub _is_maria ($self) { + $self->{_is_maria} = $self->_verbose_help =~ /\A.*MariaDB/ unless (exists $self->{_is_maria}); + return $self->{_is_maria}; +} + +sub _mysql_version ($self) { + $self->{_mysql_version} = $self->_verbose_help =~ /\A.*Ver ([0-9]+\.[0-9]+\.[0-9]+)/ + unless (exists $self->{_mysql_version}); + return $self->{_mysql_version}; +} + +sub _mysql_major_version ($self) { + my $ver = $self->_mysql_version; + return unless $ver; + return +(split /\./, $ver)[0]; +} + +sub _get_path_of ($prog) { + my $path = `which $prog 2> /dev/null`; + chomp $path if $path; + $path = '' unless -x $path; + return $path; +} + +1; diff --git a/t/lib/TestUtils.pm b/t/lib/TestUtils.pm index 7e737d49..37bba60c 100644 --- a/t/lib/TestUtils.pm +++ b/t/lib/TestUtils.pm @@ -1,98 +1,44 @@ package TestUtils; +use parent Exporter; -use warnings; -use strict; -use feature 'signatures'; -no warnings qw/experimental::signatures/; +use Mojo::Base -signatures; -use Text::CSV qw/csv/; -use DateTime::Format::Strptime; -use Mojo::JSON qw/true false/; - -require Exporter; -use base qw/Exporter/; -our @EXPORT_OK = qw/buildHash loadCSV removeIDs cleanUndef filterBySetType loadSchema/; - -my $strp_datetime = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); -my $strp_date = DateTime::Format::Strptime->new(pattern => '%F', on_error => 'croak'); +our @EXPORT_OK = qw/removeIDs cleanUndef/; =head1 DESCRIPTION This is a collection of utilities for testing purposes -=head2 buildHash - -This takes a hashref and builds up a params field and a dates field for any -field starting with PARAM: and DATE: respectively. - -=cut - -sub buildHash ($input, $config) { - my $output = {}; - for my $key (keys %{$input}) { - if ($key =~ /^([A-Z_]+):(.*)/x) { - my $field = lc($1); - my $subfield = lc($2); - $output->{$field} = {} unless defined($output->{$field}); - if ($key =~ /DATES:/) { - # Determine if each field is a date, a datetime or other (currently a boolean). - if (defined($input->{$key}) && $input->{$key} =~ /^\d{4}-\d{2}-\d{2}$/) { - my $dt = $strp_date->parse_datetime($input->{$key}); - $output->{$field}->{$subfield} = $dt->epoch; - } elsif (defined($input->{$key}) && $input->{$key} =~ /^\d{4}-\d{2}-\d{2}T\d\d:\d\d:\d\dZ$/) { - my $dt = $strp_datetime->parse_datetime($input->{$key}); - $output->{$field}->{$subfield} = $dt->epoch; - } elsif (grep {/^$subfield$/} @{ $config->{param_boolean_fields} }) { - $output->{$field}->{$subfield} = int($input->{$key}) ? true : false if defined($input->{$key}); - } - } elsif (grep { $_ eq $subfield } @{ $config->{param_boolean_fields} }) { - $output->{$field}->{$subfield} = int($input->{$key}) ? true : false if defined($input->{$key}); - } elsif (grep { $_ eq $subfield } @{ $config->{param_non_neg_int_fields} }) { - $output->{$field}->{$subfield} = int($input->{$key}) if defined($input->{$key}); - } elsif (grep { $_ eq $subfield } @{ $config->{param_non_neg_float_fields} }) { - $output->{$field}->{$subfield} = 0 + $input->{$key} if defined($input->{$key}); - } else { - $output->{$field}->{$subfield} = $input->{$key} if defined($input->{$key}); - } - } elsif (grep { $_ eq $key } @{ $config->{boolean_fields} }) { - $output->{$key} = defined($input->{$key}) && int($input->{$key}) ? true : false; - } elsif (grep { $_ eq $key } @{ $config->{non_neg_int_fields} }) { - $output->{$key} = int($input->{$key}) if defined($input->{$key}); - } elsif (grep { $_ eq $key } @{ $config->{non_neg_float_fields} }) { - $output->{$key} = 0 + $input->{$key} if defined($input->{$key}); - } else { - $output->{$key} = $input->{$key}; - } - - } - return $output; -} - -sub loadCSV ($filename, $config = {}) { - my $items_from_csv = csv(in => $filename, headers => 'auto', blank_is_undef => 1); - my @all_items = (); - for my $item (@$items_from_csv) { - push(@all_items, buildHash($item, $config)); - } - return @all_items; -} +=over -=head2 removeIDs +=item removeIDs($obj) -Removes all of the fields of an arrayref that ends in _id +Removes all fields of $obj that end in "_id" except the field "student_id". -Used for testing against items from the database with all id tags removed. +Used to remove id tags from database items for comparison with JSON data that +does not have such information. =cut # Remove any field that ends in _id except student_id and any field that has the value 'undef'. sub removeIDs ($obj) { for my $key (keys %$obj) { - delete $obj->{$key} if $key =~ /_id$/x && $key ne 'student_id'; + delete $obj->{$key} if $key =~ /_id$/ && $key ne 'student_id'; } return; } +=item cleanUndef($obj) + +Removes all fields of $obj that are undefined. + +Used to remove undefined column data from database items for comparison with +JSON data that does not have such information. + +=back + +=cut + sub cleanUndef ($obj) { for my $key (keys %$obj) { delete $obj->{$key} unless defined $obj->{$key}; @@ -100,18 +46,4 @@ sub cleanUndef ($obj) { return; } -sub filterBySetType ($all_sets, $type, $course_name) { - my $type_hash = $DB::Schema::ResultSet::ProblemSet::SET_TYPES; - my @filtered_sets = @$all_sets; - - if (defined($course_name)) { - @filtered_sets = grep { $_->{course_name} eq $course_name } @filtered_sets; - } - if (defined($type)) { - @filtered_sets = grep { $_->{set_type} eq $type } @filtered_sets; - } - - return @filtered_sets; -} - 1; diff --git a/t/mojolicious/001_login.t b/t/mojolicious/001_login.t index 7d3e3d44..bd073fd7 100644 --- a/t/mojolicious/001_login.t +++ b/t/mojolicious/001_login.t @@ -1,71 +1,70 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - use Test2::V0; +use Mojo::Base -signatures; use Test2::MojoX; -use YAML::XS qw/LoadFile/; +use Mojo::File qw/curfile/; use Mojo::JSON qw/true false/; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; -use lib "$main::ww3_dir/lib"; +use DBSubtest qw/mojoDBSubtest/; -# Load the config file. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); +mojoDBSubtest 'login routes' => sub ($t, $schema) { + # Deploy the database. + $schema->deploy({ add_drop_table => 1 }); -my $t = Test2::MojoX->new('WeBWorK3' => LoadFile($config_file)); + # Add the user for the test. + $schema->resultset('User')->create({ + email => 'lisa@google.com', + first_name => 'Lisa', + is_admin => false, + last_name => 'Simpson', + student_id => '23', + user_id => 3, + username => 'lisa', + login_params => { password => 'lisa' } + }); -# Test missing credentials -$t->post_ok('/webwork3/api/login')->status_is(500, 'error status')->content_type_is('application/json;charset=UTF-8') - ->json_is( - '' => { - exception => 'DB::Exception::ParametersNeeded', - message => 'You must pass exactly one of user_id, username, email.' - }, - 'no credentials' - ); + # Test missing credentials + $t->post_ok('/webwork3/api/login')->status_is(500, 'error status') + ->content_type_is('application/json;charset=UTF-8')->json_is( + '' => { + exception => 'DB::Exception::ParametersNeeded', + message => 'You must pass exactly one of user_id, username, email.' + }, + 'no credentials' + ); -# Test valid username and password -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is( - '' => { - logged_in => true, - user => { - email => 'lisa@google.com', - first_name => 'Lisa', - is_admin => false, - last_name => 'Simpson', - student_id => '23', - user_id => 3, - username => 'lisa' - } - }, - 'valid credentials' - ); + # Test valid username and password + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is( + '' => { + logged_in => true, + user => { + email => 'lisa@google.com', + first_name => 'Lisa', + is_admin => false, + last_name => 'Simpson', + student_id => '23', + user_id => 3, + username => 'lisa' + } + }, + 'valid credentials' + ); -# Test logout -$t->post_ok('/webwork3/api/logout')->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is( - '' => { - logged_in => false, - message => 'Successfully logged out.' - }, - 'logout' -); + # Test logout + $t->post_ok('/webwork3/api/logout')->status_is(200)->content_type_is('application/json;charset=UTF-8') + ->json_is('' => { logged_in => false, message => 'Successfully logged out.' }, 'logout'); -# Test for a bad password -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'wrong_password' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is( - '' => { - logged_in => false, - message => 'Incorrect username or password.' - }, - 'invalid credentials' - ); + # Test for a bad password + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'wrong_password' }) + ->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is( + '' => { logged_in => false, message => 'Incorrect username or password.' }, + 'invalid credentials' + ); +}; done_testing; diff --git a/t/mojolicious/002_courses.t b/t/mojolicious/002_courses.t index e0f07a74..338e2df8 100644 --- a/t/mojolicious/002_courses.t +++ b/t/mojolicious/002_courses.t @@ -1,131 +1,123 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - use Test2::V0; +use Mojo::Base -signatures; use Test2::MojoX; +use Mojo::File qw/curfile/; use Mojo::JSON qw/true false/; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; - -use Clone qw/clone/; -use YAML::XS qw/LoadFile/; +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; -# Test the api with common 'courses' routes. +use DBSubtest qw/mojoDBSubtest/; +use BuildDB qw/loadPermissions addCourses addUsers/; -# Load the config file. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = clone(LoadFile($config_file)); +mojoDBSubtest 'courses routes' => sub ($t, $schema) { + # Add the neccessary sample data for the test. + loadPermissions($schema, $t->app->home); + addCourses($schema, $t->app->home); + addUsers($schema, $t->app->home); -my $t = Test2::MojoX->new(WeBWorK3 => $config); + # Authenticate with the admin user. + $t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/user_id' => 1)->json_is('/user/is_admin' => true); -# Authenticate with the admin user. -$t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true)->json_is('/user/user_id' => 1) - ->json_is('/user/is_admin' => true); + $t->get_ok('/webwork3/api/courses')->content_type_is('application/json;charset=UTF-8') + ->json_is('/0/course_name' => 'Precalculus')->json_is('/0/visible' => true); -$t->get_ok('/webwork3/api/courses')->content_type_is('application/json;charset=UTF-8') - ->json_is('/0/course_name' => 'Precalculus')->json_is('/0/visible' => true); + $t->get_ok('/webwork3/api/courses/1')->content_type_is('application/json;charset=UTF-8') + ->json_is('/course_name' => 'Precalculus')->json_is('/visible' => true); -$t->get_ok('/webwork3/api/courses/1')->content_type_is('application/json;charset=UTF-8') - ->json_is('/course_name' => 'Precalculus')->json_is('/visible' => true); - -# Add a new course -my $new_course = { - course_name => 'Linear Algebra', - course_dates => { start => '2021-05-31', end => '2021-07-01' } -}; + # Add a new course + my $new_course = { + course_name => 'Linear Algebra', + course_dates => { start => '2021-05-31', end => '2021-07-01' } + }; -$t->post_ok('/webwork3/api/courses' => json => $new_course)->status_is(200) - ->json_is('/course_name' => $new_course->{course_name}); + $t->post_ok('/webwork3/api/courses' => json => $new_course)->status_is(200) + ->json_is('/course_name' => $new_course->{course_name}); -# Extract the id from the response. -my $new_course_id = $t->tx->res->json('/course_id'); + # Extract the id from the response. + my $new_course_id = $t->tx->res->json('/course_id'); -$new_course->{course_id} = $new_course_id; -# The default for visible is true: -$new_course->{visible} = true; -is($t->tx->res->json, $new_course, "addCourse: courses match"); + $new_course->{course_id} = $new_course_id; + # The default for visible is true: + $new_course->{visible} = true; + is($t->tx->res->json, $new_course, "addCourse: courses match"); -# Update the course -$new_course->{visible} = true; -$t->put_ok("/webwork3/api/courses/$new_course_id" => json => $new_course)->status_is(200) - ->json_is('/course_name' => $new_course->{course_name}); + # Update the course + $new_course->{visible} = true; + $t->put_ok("/webwork3/api/courses/$new_course_id" => json => $new_course)->status_is(200) + ->json_is('/course_name' => $new_course->{course_name}); -is($t->tx->res->json, $new_course, 'updateCourse: courses match'); + is($t->tx->res->json, $new_course, 'updateCourse: courses match'); -# Testing that booleans returned from the server are JSON booleans. -# getting the first course + # Testing that booleans returned from the server are JSON booleans. + # getting the first course -# to check this, relogin as admin -$t->post_ok('/webwork3/api/logout')->status_is(200); -$t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200); + # to check this, relogin as admin + $t->post_ok('/webwork3/api/logout')->status_is(200); + $t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200); -$t->get_ok('/webwork3/api/courses/1')->json_is('/course_name', 'Precalculus'); -my $precalc = $t->tx->res->json; + $t->get_ok('/webwork3/api/courses/1')->json_is('/course_name', 'Precalculus'); + my $precalc = $t->tx->res->json; -ok($precalc->{visible}, 'Testing that visible field is truthy.'); -is($precalc->{visible}, true, 'Testing that the visible field compares to JSON::true'); -ok(JSON::PP::is_bool($precalc->{visible}), 'Testing that the visible field is a JSON boolean'); -ok(JSON::PP::is_bool($precalc->{visible}) && $precalc->{visible}, 'testing that the visible field is a JSON::true'); + ok($precalc->{visible}, 'Testing that visible field is truthy.'); + is($precalc->{visible}, true, 'Testing that the visible field compares to JSON::true'); + ok(JSON::PP::is_bool($precalc->{visible}), 'Testing that the visible field is a JSON boolean'); + ok(JSON::PP::is_bool($precalc->{visible}) && $precalc->{visible}, + 'testing that the visible field is a JSON::true'); -ok(not(JSON::PP::is_bool($precalc->{course_id})), 'testing that $precalc->{visible} is not a JSON boolean'); + ok(not(JSON::PP::is_bool($precalc->{course_id})), 'testing that $precalc->{visible} is not a JSON boolean'); -# Test for exceptions + # Test for exceptions -# A set that is not in a course. -$t->get_ok('/webwork3/api/courses/99999')->status_is(500, 'error status') - ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::CourseNotFound'); + # A set that is not in a course. + $t->get_ok('/webwork3/api/courses/99999')->status_is(500, 'error status') + ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::CourseNotFound'); -# Try to update a non-existent course -$t->put_ok('/webwork3/api/courses/999999' => json => { course_name => 'new course name' }) - ->status_is(500, 'error status')->content_type_is('application/json;charset=UTF-8') - ->json_is('/exception' => 'DB::Exception::CourseNotFound'); + # Try to update a non-existent course + $t->put_ok('/webwork3/api/courses/999999' => json => { course_name => 'new course name' }) + ->status_is(500, 'error status')->content_type_is('application/json;charset=UTF-8') + ->json_is('/exception' => 'DB::Exception::CourseNotFound'); -# Try to add a course without a course_name. -my $another_new_course = { name => 'this is the wrong field' }; + # Try to add a course without a course_name. + my $another_new_course = { name => 'this is the wrong field' }; -$t->post_ok('/webwork3/api/courses/' => json => $another_new_course)->status_is(500, 'error status') - ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::ParametersNeeded'); + $t->post_ok('/webwork3/api/courses/' => json => $another_new_course)->status_is(500, 'error status') + ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::ParametersNeeded'); -# Try to delete a non-existent course. -$t->delete_ok('/webwork3/api/courses/9999999')->status_is(500, 'error status') - ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::CourseNotFound'); + # Try to delete a non-existent course. + $t->delete_ok('/webwork3/api/courses/9999999')->status_is(500, 'error status') + ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::CourseNotFound'); -# Delete the added course. -$t->delete_ok("/webwork3/api/courses/$new_course_id")->status_is(200) - ->json_is('/course_name' => $new_course->{course_name}); + # Delete the added course. + $t->delete_ok("/webwork3/api/courses/$new_course_id")->status_is(200) + ->json_is('/course_name' => $new_course->{course_name}); -# Logout of the admin user account and relogin as a non-admin: + # Logout of the admin user account and relogin as a non-admin: + $t->post_ok('/webwork3/api/logout')->status_is(200); + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) + ->status_is(200)->json_is('/logged_in' => true)->json_is('/user/username' => 'lisa') + ->json_is('/user/is_admin' => false); -$t->post_ok('/webwork3/api/logout')->status_is(200); -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->status_is(200)->json_is('/logged_in' => true)->json_is('/user/username' => 'lisa') - ->json_is('/user/is_admin' => false); + # an instructor can get information about the given course. + $t->get_ok('/webwork3/api/courses/4')->status_is(200)->json_is('/course_name' => 'Arithmetic'); -# an instructor can get information about the given course. -$t->get_ok('/webwork3/api/courses/4')->status_is(200)->json_is('/course_name' => 'Arithmetic'); + # and also the settings for the course. + $t->get_ok('/webwork3/api/courses/4/default_settings')->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); + $t->get_ok('/webwork3/api/courses/4/settings')->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); -# and also the settings for the course. + # The user with role instructor should not have permissions for the following routes. + $t->post_ok('/webwork3/api/courses' => json => $new_course)->status_is(403)->json_is('/has_permission' => 0); -$t->get_ok('/webwork3/api/courses/4/default_settings')->status_is(200) - ->content_type_is('application/json;charset=UTF-8'); -$t->get_ok('/webwork3/api/courses/4/settings')->status_is(200)->content_type_is('application/json;charset=UTF-8'); + $t->put_ok('/webwork3/api/courses/4' => json => { course_name => 'XXX' })->status_is(403) + ->json_is('/has_permission' => 0); -# The user with role instructor should not have permissions for the following routes. - -$t->post_ok('/webwork3/api/courses' => json => $new_course)->status_is(403)->json_is('/has_permission' => 0); - -$t->put_ok('/webwork3/api/courses/4' => json => { course_name => 'XXX' })->status_is(403) - ->json_is('/has_permission' => 0); - -$t->delete_ok('/webwork3/api/courses/4')->status_is(403)->json_is('/has_permission' => 0); + $t->delete_ok('/webwork3/api/courses/4')->status_is(403)->json_is('/has_permission' => 0); +}; done_testing; diff --git a/t/mojolicious/003_users.t b/t/mojolicious/003_users.t index 949a2638..9fb29a89 100644 --- a/t/mojolicious/003_users.t +++ b/t/mojolicious/003_users.t @@ -1,215 +1,195 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - use Test2::V0; +use Mojo::Base -signatures; use Test2::MojoX; +use Mojo::File qw/curfile/; use Mojo::JSON qw/true false/; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; -use DB::Schema; -use Clone qw/clone/; -use YAML::XS qw/LoadFile/; +use DBSubtest qw/mojoDBSubtest/; +use BuildDB qw/loadPermissions addCourses addUsers/; -# Test the api with common 'users' routes. +mojoDBSubtest 'users routes' => sub ($t, $schema) { + # Add the neccessary sample data for the test. + loadPermissions($schema, $t->app->home); + addCourses($schema, $t->app->home); + addUsers($schema, $t->app->home); -# Load the config file. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = clone(LoadFile($config_file)); + # Test all of the user routes with an admin user. + $t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/user_id' => 1)->json_is('/user/is_admin' => true); -# Connect to the database. -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); + my @all_users = $schema->resultset('User')->getAllGlobalUsers(); + + $t->get_ok('/webwork3/api/users')->status_is(200)->content_type_is('application/json;charset=UTF-8') + ->json_is('/1/first_name' => $all_users[1]->{first_name})->json_is('/1/email' => $all_users[1]->{email}); -my $t = Test2::MojoX->new(WeBWorK3 => $config); + $t->get_ok('/webwork3/api/users/2')->status_is(200)->content_type_is('application/json;charset=UTF-8') + ->json_is('/username' => 'lisa')->json_is('/email' => 'lisa@google.com'); -# Test all of the user routes with an admin user. + # Add a new user. + my $new_user = { + email => 'maggie@abc.com', + first_name => 'Maggie', + last_name => 'Simpson', + username => 'maggie', + student_id => '1234123423', + is_admin => false + }; + + $t->post_ok('/webwork3/api/users' => json => $new_user)->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/username' => $new_user->{username}); -$t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true)->json_is('/user/user_id' => 1) - ->json_is('/user/is_admin' => true); + # Extract the id from the response. + my $new_user_from_db = $t->tx->res->json; -my @all_users = $schema->resultset('User')->getAllGlobalUsers(); + $new_user->{user_id} = $new_user_from_db->{user_id}; + is($new_user_from_db, $new_user, 'addUser: global user added.'); -$t->get_ok('/webwork3/api/users')->status_is(200)->content_type_is('application/json;charset=UTF-8') - ->json_is('/1/first_name' => $all_users[1]->{first_name})->json_is('/1/email' => $all_users[1]->{email}); + # Update the user. + $new_user->{email} = 'maggie@juno.com'; + $t->put_ok("/webwork3/api/users/$new_user->{user_id}" => json => $new_user)->status_is(200); + is($t->tx->res->json, $new_user, 'updateUser: global user updated'); -$t->get_ok('/webwork3/api/users/3')->status_is(200)->content_type_is('application/json;charset=UTF-8') - ->json_is('/username' => 'lisa')->json_is('/email' => 'lisa@google.com'); + # Add the user to the course. + my $added_user_to_course = { user_id => $new_user->{user_id}, role => 'student' }; + $t->post_ok('/webwork3/api/courses/4/users' => json => $added_user_to_course)->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/role' => 'student'); -# Add a new user. -my $new_user = { - email => 'maggie@abc.com', - first_name => 'Maggie', - last_name => 'Simpson', - username => 'maggie', - student_id => '1234123423', - is_admin => false -}; + my $lisa = (grep { $_->{username} eq 'lisa' } @all_users)[0]; -$t->post_ok('/webwork3/api/users' => json => $new_user)->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/username' => $new_user->{username}); + # Check if the user is a global user + $t->get_ok('/webwork3/api/courses/1/users/lisa/exists')->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/first_name' => $lisa->{first_name}); -# Extract the id from the response. -my $new_user_from_db = $t->tx->res->json; + # Check if a non-existent user is a global user + $t->get_ok('/webwork3/api/courses/1/users/non_existent_user/exists')->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); -$new_user->{user_id} = $new_user_from_db->{user_id}; -is($new_user_from_db, $new_user, 'addUser: global user added.'); + is($t->tx->res->json, {}, 'checkUserExists: check that a non-existent user returns {}'); -# Update the user. -$new_user->{email} = 'maggie@juno.com'; -$t->put_ok("/webwork3/api/users/$new_user->{user_id}" => json => $new_user)->status_is(200); -is($t->tx->res->json, $new_user, 'updateUser: global user updated'); + # Testing that booleans returned from the server are JSON booleans. + # the first user is the admin -# Add the user to the course. -my $added_user_to_course = { - user_id => $new_user->{user_id}, - role => 'student' -}; -$t->post_ok('/webwork3/api/courses/4/users' => json => $added_user_to_course)->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/role' => 'student'); + my $admin_user = (grep { $_->{username} eq 'admin' } @all_users)[0]; + ok(not(JSON::PP::is_bool($admin_user->{user_id})), 'testing that $admin->{user_id} is not a JSON boolean'); -my $lisa = (grep { $_->{username} eq 'lisa' } @all_users)[0]; + ok(!$new_user_from_db->{is_admin}, 'testing new_user->{is_admin} is not truthy.'); + is($new_user_from_db->{is_admin}, false, 'testing that new_user->{is_admin} compares to false'); + ok(JSON::PP::is_bool($new_user_from_db->{is_admin}), 'testing that new_user->{is_admin} is a true or false'); + ok(JSON::PP::is_bool($new_user_from_db->{is_admin}) && !$new_user_from_db->{is_admin}, + 'testing that new_user->{is_admin} is a false'); -# Check if the user is a global user -$t->get_ok('/webwork3/api/courses/1/users/lisa/exists')->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/first_name' => $lisa->{first_name}); + # Test for exceptions -# Check if a non-existent user is a global user -$t->get_ok('/webwork3/api/courses/1/users/non_existent_user/exists')->status_is(200) - ->content_type_is('application/json;charset=UTF-8'); + # Try to get a non-existent user. + $t->get_ok('/webwork3/api/users/99999')->content_type_is('application/json;charset=UTF-8') + ->status_is(500, 'exception status')->json_is('/exception' => 'DB::Exception::UserNotFound'); -is($t->tx->res->json, {}, 'checkUserExists: check that a non-existent user returns {}'); + # Try to update a user not in a course. + $t->put_ok('/webwork3/api/users/99999' => json => { email => 'fred@happy.com' }) + ->status_is(500, 'exception status')->content_type_is('application/json;charset=UTF-8') + ->json_is('/exception' => 'DB::Exception::UserNotFound'); -# Testing that booleans returned from the server are JSON booleans. -# the first user is the admin + # Try to add a user without a username. + my $another_new_user = { username_name => 'this is the wrong field' }; + $t->post_ok('/webwork3/api/users' => json => $another_new_user) + ->content_type_is('application/json;charset=UTF-8')->status_is(500, 'exception status') + ->json_is('/exception' => 'DB::Exception::ParametersNeeded'); -my $admin_user = (grep { $_->{username} eq 'admin' } @all_users)[0]; -ok(not(JSON::PP::is_bool($admin_user->{user_id})), 'testing that $admin->{user_id} is not a JSON boolean'); + # Try to delete a user not in a course. + $t->delete_ok('/webwork3/api/users/99999')->content_type_is('application/json;charset=UTF-8') + ->status_is(500, 'exception status')->json_is('/exception' => 'DB::Exception::UserNotFound'); -ok(!$new_user_from_db->{is_admin}, 'testing new_user->{is_admin} is not truthy.'); -is($new_user_from_db->{is_admin}, false, 'testing that new_user->{is_admin} compares to false'); -ok(JSON::PP::is_bool($new_user_from_db->{is_admin}), 'testing that new_user->{is_admin} is a true or false'); -ok(JSON::PP::is_bool($new_user_from_db->{is_admin}) && !$new_user_from_db->{is_admin}, - 'testing that new_user->{is_admin} is a false'); + # Add another user to a course that is not a global user. + my $another_user = { + username => 'bob', + first_name => 'Sideshow', + last_name => 'Bob', + student_id => '933723', + email => 'bob@sideshow.net' + }; -# Test for exceptions + $t->post_ok('/webwork3/api/users' => json => $another_user)->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/username' => $another_user->{username}); -# Try to get a non-existent user. -$t->get_ok('/webwork3/api/users/99999')->content_type_is('application/json;charset=UTF-8') - ->status_is(500, 'exception status')->json_is('/exception' => 'DB::Exception::UserNotFound'); + my $another_user_id = $t->tx->res->json('/user_id'); -# Try to update a user not in a course. -$t->put_ok('/webwork3/api/users/99999' => json => { email => 'fred@happy.com' })->status_is(500, 'exception status') - ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::UserNotFound'); + $t->post_ok( + '/webwork3/api/courses/4/users' => json => { + user_id => $another_user_id, + role => 'student' + } + )->status_is(200)->content_type_is('application/json;charset=UTF-8'); -# Try to add a user without a username. -my $another_new_user = { username_name => 'this is the wrong field' }; -$t->post_ok('/webwork3/api/users' => json => $another_new_user)->content_type_is('application/json;charset=UTF-8') - ->status_is(500, 'exception status')->json_is('/exception' => 'DB::Exception::ParametersNeeded'); - -# Try to delete a user not in a course. -$t->delete_ok('/webwork3/api/users/99999')->content_type_is('application/json;charset=UTF-8') - ->status_is(500, 'exception status')->json_is('/exception' => 'DB::Exception::UserNotFound'); - -# Add another user to a course that is not a global user. -my $another_user = { - username => 'bob', - first_name => 'Sideshow', - last_name => 'Bob', - student_id => '933723', - email => 'bob@sideshow.net' -}; + my $another_new_user_id = $t->tx->res->json('/user_id'); -$t->post_ok('/webwork3/api/users' => json => $another_user)->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/username' => $another_user->{username}); + # For cleanup, delete the created users. Need to relogin as an admin: + $t->delete_ok("/webwork3/api/users/$new_user_from_db->{user_id}")->status_is(200) + ->json_is('/username' => $new_user->{username}); + $t->delete_ok("/webwork3/api/users/$another_new_user_id")->status_is(200) + ->json_is('/username' => $another_user->{username}); -my $another_user_id = $t->tx->res->json('/user_id'); + # Test that a non-admin user cannot access all of the routes + # Logout the admin user and relogin as a non-admin. -$t->post_ok( - '/webwork3/api/courses/4/users' => json => { - user_id => $another_user_id, - role => 'student' - } -)->status_is(200)->content_type_is('application/json;charset=UTF-8'); + $t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); -my $another_new_user_id = $t->tx->res->json('/user_id'); + $t->get_ok('/webwork3/api/users')->content_type_is('application/json;charset=UTF-8')->status_is(403) + ->json_is('/has_permission' => 0); -# For cleanup, delete the created users. Need to relogin as an admin: + $t->get_ok('/webwork3/api/users/1')->content_type_is('application/json;charset=UTF-8')->status_is(403) + ->json_is('/has_permission' => 0); -$t->delete_ok("/webwork3/api/users/$new_user_from_db->{user_id}")->status_is(200) - ->json_is('/username' => $new_user->{username}); + $t->post_ok('/webwork3/api/users' => json => $new_user)->content_type_is('application/json;charset=UTF-8') + ->status_is(403)->json_is('/has_permission' => 0); -$t->delete_ok("/webwork3/api/users/$another_new_user_id")->status_is(200) - ->json_is('/username' => $another_user->{username}); + $t->put_ok('/webwork3/api/users/1' => json => { email => 'lisa@aol.com' })->status_is(403) + ->content_type_is('application/json;charset=UTF-8')->json_is('/has_permission' => 0); -# Test that a non-admin user cannot access all of the routes -# Logout the admin user and relogin as a non-admin. + $t->delete_ok('/webwork3/api/users/1')->content_type_is('application/json;charset=UTF-8')->status_is(403) + ->json_is('/has_permission' => 0); -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) - ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); + # Test that a user can access their own courses. Lisa has user_id 2. + $t->get_ok('/webwork3/api/users/2/courses')->status_is(200)->content_type_is('application/json;charset=UTF-8'); -$t->get_ok('/webwork3/api/users')->content_type_is('application/json;charset=UTF-8')->status_is(403) - ->json_is('/has_permission' => 0); + # Relogin as the admin and delete the added users + $t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); + $t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200); -$t->get_ok('/webwork3/api/users/1')->content_type_is('application/json;charset=UTF-8')->status_is(403) - ->json_is('/has_permission' => 0); + # The following routes test that global users can be handled by an instructor in the course + # Lisa is an instructor in the Arithmetic course (course_id => 4) -$t->post_ok('/webwork3/api/users' => json => $new_user)->content_type_is('application/json;charset=UTF-8') - ->status_is(403)->json_is('/has_permission' => 0); + my $new_global_user = { + username => 'maggie', + first_name => 'Maggie', + last_name => 'Simpson', + student_id => 1234, + email => 'maggie@thesimpsons.tv' + }; -$t->put_ok('/webwork3/api/users/1' => json => { email => 'lisa@aol.com' })->status_is(403) - ->content_type_is('application/json;charset=UTF-8')->json_is('/has_permission' => 0); + $t->post_ok('/webwork3/api/courses/4/global-users' => json => $new_global_user)->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/username' => 'maggie'); -$t->delete_ok('/webwork3/api/users/1')->content_type_is('application/json;charset=UTF-8')->status_is(403) - ->json_is('/has_permission' => 0); + my $new_global_user_id = $t->tx->res->json('/user_id'); -# Test that a user can access their own courses. Lisa has user_id 3. -$t->get_ok('/webwork3/api/users/3/courses')->status_is(200)->content_type_is('application/json;charset=UTF-8'); + $t->get_ok("/webwork3/api/courses/4/global-users/$new_global_user_id")->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/username' => 'maggie') + ->json_is('/student_id' => 1234); -# Relogin as the admin and delete the added users -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); -$t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200); + $t->put_ok("/webwork3/api/courses/4/global-users/$new_global_user_id" => json => { student_id => 4321 }) + ->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/username' => 'maggie') + ->json_is('/student_id' => 4321); -# The following routes test that global users can be handled by an instructor in the course -# Lisa is an instructor in the Arithmetic course (course_id => 4) - -my $new_global_user = { - username => 'maggie', - first_name => 'Maggie', - last_name => 'Simpson', - student_id => 1234, - email => 'maggie@thesimpsons.tv' + $t->delete_ok("/webwork3/api/courses/4/global-users/$new_global_user_id")->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); }; -$t->post_ok('/webwork3/api/courses/4/global-users' => json => $new_global_user)->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/username' => 'maggie'); - -my $new_global_user_id = $t->tx->res->json('/user_id'); - -$t->get_ok("/webwork3/api/courses/4/global-users/$new_global_user_id")->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/username' => 'maggie') - ->json_is('/student_id' => 1234); - -$t->put_ok("/webwork3/api/courses/4/global-users/$new_global_user_id" => json => { student_id => 4321 }) - ->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/username' => 'maggie') - ->json_is('/student_id' => 4321); - -$t->delete_ok("/webwork3/api/courses/4/global-users/$new_global_user_id")->status_is(200) - ->content_type_is('application/json;charset=UTF-8'); - done_testing; diff --git a/t/mojolicious/004_course_users.t b/t/mojolicious/004_course_users.t index 4551269c..48878e96 100644 --- a/t/mojolicious/004_course_users.t +++ b/t/mojolicious/004_course_users.t @@ -1,165 +1,141 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - use Test2::V0; +use Mojo::Base -signatures; use Test2::MojoX; +use Mojo::File qw/curfile/; use Mojo::JSON qw/true false/; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; - -use DB::Schema; -use Clone qw/clone/; -use YAML::XS qw/LoadFile/; - -# Test the api with common 'courses/users' routes. - -# Load the config file. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = clone(LoadFile($config_file)); - -# Connect to the database. -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -my $t = Test2::MojoX->new(WeBWorK3 => $config); - -# Login as an instructor in the Arithmetic course (course_id: 4) -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) - ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); - -# Get all course users in course_id: 4 -$t->get_ok('/webwork3/api/courses/4/global-courseusers')->status_is(200) - ->content_type_is('application/json;charset=UTF-8'); -my $all_users = $t->tx->res->json; - -$t->get_ok('/webwork3/api/courses/4/users')->status_is(200)->content_type_is('application/json;charset=UTF-8') - ->json_is('/0/role' => 'instructor')->json_is('/1/role' => 'student'); - -# Extract id from the response. -my $user_id = $t->tx->res->json('/1/user_id'); - -$t->get_ok("/webwork3/api/courses/4/users/$user_id")->status_is(200)->content_type_is('application/json;charset=UTF-8') - ->json_is('/user_id' => $user_id)->json_is('/role' => 'student'); - -# Add a new global user. -my $new_user = { - email => 'maggie@abc.com', - first_name => 'Maggie', - last_name => 'Simpson', - username => 'maggie', - student_id => '1234123423' -}; -$t->post_ok('/webwork3/api/courses/4/global-users' => json => $new_user)->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/first_name' => $new_user->{first_name}) - ->json_is('/email' => $new_user->{email}); - -# Add the user to a course. -my $new_user_id = $t->tx->res->json('/user_id'); -my $course_user_params = { - user_id => $new_user_id, - role => 'student', - course_user_params => { - comment => "I love my big sister", - useMathQuill => true - } +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use DBSubtest qw/mojoDBSubtest/; +use BuildDB qw/loadPermissions addCourses addUsers/; + +mojoDBSubtest 'course users routes' => sub ($t, $schema) { + # Add the neccessary sample data for the test. + loadPermissions($schema, $t->app->home); + addCourses($schema, $t->app->home); + addUsers($schema, $t->app->home); + + # Login as an instructor in the Arithmetic course (course_id: 4) + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); + + # Get all course users in course_id: 4 + $t->get_ok('/webwork3/api/courses/4/global-courseusers')->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); + my $all_users = $t->tx->res->json; + + $t->get_ok('/webwork3/api/courses/4/users')->status_is(200)->content_type_is('application/json;charset=UTF-8') + ->json_is('/0/role' => 'instructor')->json_is('/1/role' => 'student'); + + # Extract id from the response. + my $user_id = $t->tx->res->json('/1/user_id'); + + $t->get_ok("/webwork3/api/courses/4/users/$user_id")->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/user_id' => $user_id) + ->json_is('/role' => 'student'); + + # Add a new global user. + my $new_user = { + email => 'maggie@abc.com', + first_name => 'Maggie', + last_name => 'Simpson', + username => 'maggie', + student_id => '1234123423' + }; + $t->post_ok('/webwork3/api/courses/4/global-users' => json => $new_user)->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/first_name' => $new_user->{first_name}) + ->json_is('/email' => $new_user->{email}); + + # Add the user to a course. + my $new_user_id = $t->tx->res->json('/user_id'); + my $course_user_params = { + user_id => $new_user_id, + role => 'student', + course_user_params => { comment => "I love my big sister", useMathQuill => true } + }; + + $t->post_ok('/webwork3/api/courses/4/users' => json => $course_user_params)->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/role' => $course_user_params->{role}) + ->json_is('/course_user_params/comment' => $course_user_params->{course_user_params}{comment}); + + my $added_user = $t->tx->res->json; + + # Update the new user. + $new_user->{recitation} = 2; + $t->put_ok("/webwork3/api/courses/4/users/$new_user_id" => json => { recitation => 2 })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/recitation' => 2); + + # Test that the booleans are being sent as JSON booleans: + ok($added_user->{course_user_params}{useMathQuill}, 'Testing that useMathQuill param is truthy.'); + is($added_user->{course_user_params}{useMathQuill}, + true, 'Testing that the useMathQuill param compares to JSON::true'); + ok( + JSON::PP::is_bool($added_user->{course_user_params}{useMathQuill}), + 'Testing that the useMathQuill param is a JSON boolean' + ); + ok( + JSON::PP::is_bool($added_user->{course_user_params}{useMathQuill}) + && $added_user->{course_user_params}{useMathQuill}, + 'testing that the useMathQuill param is a JSON::true' + ); + + # Test for exceptions + + # A non-existent user + $t->get_ok('/webwork3/api/courses/4/users/99999')->status_is(500, 'status for exception') + ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::UserNotFound'); + + # Try to get a user that is not in a course. + $t->get_ok("/webwork3/api/courses/4/users/6")->status_is(500, 'status for exception') + ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::UserNotInCourse'); + + # Try to update a user that is not in a course. + $t->put_ok("/webwork3/api/courses/4/users/6" => json => { recitation => '2' }) + ->status_is(500, 'status for exception')->content_type_is('application/json;charset=UTF-8') + ->json_is('/exception' => 'DB::Exception::UserNotInCourse'); + + # Try to add a user without a username. + $t->post_ok('/webwork3/api/courses/4/users' => json => { username_name => 'this is the wrong field' }) + ->status_is(500, 'status for exception')->content_type_is('application/json;charset=UTF-8') + ->json_is('/exception' => 'DB::Exception::ParametersNeeded'); + + # Try to delete a user that is not found. + $t->delete_ok('/webwork3/api/courses/4/users/99')->status_is(500, 'status for exception') + ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::UserNotFound'); + + # Try to delete a user that is not in a course. + $t->delete_ok("/webwork3/api/courses/4/users/6")->status_is(500, 'status for exception') + ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::UserNotInCourse'); + + # Delete the added course user + $t->delete_ok("/webwork3/api/courses/4/users/$new_user_id")->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/user_id' => $new_user_id); + + # Check that a student doesn't have the same access as an instructor + # The user lisa is a student in the 'Topology' course (course_id => 3) + # This checks that she doesn't have the instructor access to this course. + $t->get_ok('/webwork3/api/courses/3/users')->status_is(403)->content_type_is('application/json;charset=UTF-8'); + + $t->post_ok('/webwork3/api/courses/3/users' => json => { user_id => $new_user_id, role => 'student' }) + ->status_is(403)->content_type_is('application/json;charset=UTF-8'); + + $t->put_ok('/webwork3/api/courses/3/users/' . $new_user_id => json => { recitation => 4 })->status_is(403) + ->content_type_is('application/json;charset=UTF-8'); + + $t->delete_ok('/webwork3/api/courses/3/users/' . $new_user_id)->status_is(403) + ->content_type_is('application/json;charset=UTF-8'); + + # Delete the added global user, but need to relogin as admin + $t->post_ok('/webwork3/api/logout')->status_is(200); + $t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200); + + # Delete the added users. + $t->delete_ok("/webwork3/api/users/$new_user_id")->status_is(200) + ->json_is('/username' => $new_user->{username}); }; -$t->post_ok('/webwork3/api/courses/4/users' => json => $course_user_params)->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/role' => $course_user_params->{role}) - ->json_is('/course_user_params/comment' => $course_user_params->{course_user_params}->{comment}); - -my $added_user = $t->tx->res->json; - -# Update the new user. -$new_user->{recitation} = 2; -$t->put_ok("/webwork3/api/courses/4/users/$new_user_id" => json => { recitation => 2 })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/recitation' => 2); - -# Test that the booleans are being sent as JSON booleans: -ok($added_user->{course_user_params}->{useMathQuill}, 'Testing that useMathQuill param is truthy.'); -is($added_user->{course_user_params}->{useMathQuill}, - true, 'Testing that the useMathQuill param compares to JSON::true'); -ok( - JSON::PP::is_bool($added_user->{course_user_params}->{useMathQuill}), - 'Testing that the useMathQuill param is a JSON boolean' -); -ok( - JSON::PP::is_bool($added_user->{course_user_params}->{useMathQuill}) - && $added_user->{course_user_params}->{useMathQuill}, - 'testing that the useMathQuill param is a JSON::true' -); - -# Test for exceptions - -# A non-existent user -$t->get_ok('/webwork3/api/courses/4/users/99999')->status_is(500, 'status for exception') - ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::UserNotFound'); - -# Check that a new user is not in a course. -$t->get_ok("/webwork3/api/courses/4/users/5")->status_is(500, 'status for exception') - ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::UserNotInCourse'); - -# Try to update a user that is not in a course. -$t->put_ok("/webwork3/api/courses/4/users/5" => json => { recitation => '2' })->status_is(500, 'status for exception') - ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::UserNotInCourse'); - -# Try to add a user without a username. -$t->post_ok('/webwork3/api/courses/4/users' => json => { username_name => 'this is the wrong field' }) - ->status_is(500, 'status for exception')->content_type_is('application/json;charset=UTF-8') - ->json_is('/exception' => 'DB::Exception::ParametersNeeded'); - -# Try to delete a user that is not found. -$t->delete_ok('/webwork3/api/courses/4/users/99')->status_is(500, 'status for exception') - ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::UserNotFound'); - -# Try to delete a user that is not in a course. -$t->delete_ok("/webwork3/api/courses/4/users/5")->status_is(500, 'status for exception') - ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::UserNotInCourse'); - -# Delete the added course user -$t->delete_ok("/webwork3/api/courses/4/users/$new_user_id")->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/user_id' => $new_user_id); - -# Check that a student doesn't have the same access as an instructor -# The user lisa is a student in the 'Topology' course (course_id => 3) -# This checks that she doesn't have the instructor access to this course. -$t->get_ok('/webwork3/api/courses/3/users')->status_is(403)->content_type_is('application/json;charset=UTF-8'); - -$t->post_ok( - '/webwork3/api/courses/3/users' => json => { - user_id => $new_user_id, - role => 'student' - } -)->status_is(403)->content_type_is('application/json;charset=UTF-8'); - -$t->put_ok( - '/webwork3/api/courses/3/users/' - . $new_user_id => json => { - recitation => 4 - } -)->status_is(403)->content_type_is('application/json;charset=UTF-8'); - -$t->delete_ok('/webwork3/api/courses/3/users/' . $new_user_id)->status_is(403) - ->content_type_is('application/json;charset=UTF-8'); - -# Delete the added global user, but need to relogin as admin -$t->post_ok('/webwork3/api/logout')->status_is(200); -$t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200); - -# Delete the added users. -$t->delete_ok("/webwork3/api/users/$new_user_id")->status_is(200)->json_is('/username' => $new_user->{username}); - done_testing; diff --git a/t/mojolicious/005_problem_sets.t b/t/mojolicious/005_problem_sets.t index 60528298..cb094145 100644 --- a/t/mojolicious/005_problem_sets.t +++ b/t/mojolicious/005_problem_sets.t @@ -1,151 +1,123 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - use Test2::V0; +use Mojo::Base -signatures; use Test2::MojoX; +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json/; use Mojo::JSON qw/true false/; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - -use DB::Schema; -use Clone qw/clone/; -use YAML::XS qw/LoadFile/; -use TestUtils qw/loadCSV/; - -# Test the api with common 'courses/sets' routes. - -# Load the config file. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = clone(LoadFile($config_file)); - -# Connect to the database. -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); -my $t = Test2::MojoX->new(WeBWorK3 => $config); - -# Login as an instructor. Lisa is an instructor in course_id: 4 (Arithmetic) -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) - ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); - -# Load the homework sets. -my @hw_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/hw_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => [ 'enable_reduced_scoring', 'hide_hint' ] +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use DBSubtest qw/mojoDBSubtest/; +use BuildDB qw/loadPermissions addCourses addUsers addSets/; + +mojoDBSubtest 'course/sets routes' => sub ($t, $schema) { + # Add the neccessary sample data for the test. + loadPermissions($schema, $t->app->home); + addCourses($schema, $t->app->home); + addUsers($schema, $t->app->home); + addSets($schema, $t->app->home); + + # Login as an instructor. Lisa is an instructor in course_id: 4 (Arithmetic) + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); + + # Load HW sets from JSON file. + my $course_hw_sets = decode_json($t->app->home->child('t/db/sample_data/hw_sets.json')->slurp); + my @hw_sets; + for my $course_data (@$course_hw_sets) { + for my $hw_set (@{ $course_data->{sets} }) { + $hw_set->{set_type} = 'HW'; + $hw_set->{course_name} = $course_data->{course_name}; + push(@hw_sets, $hw_set); + } } -); -for my $set (@hw_sets) { - $set->{set_type} = 'HW'; -} - -my @arith_hw = grep { $_->{course_name} eq 'Arithmetic' } @hw_sets; - -$t->get_ok('/webwork3/api/courses/4/sets')->status_is(200)->content_type_is('application/json;charset=UTF-8') - ->json_is('/1/set_name' => $arith_hw[1]->{set_name}) - ->json_is('/1/set_dates/open' => $arith_hw[1]->{set_dates}->{open}); - -my $arith_hw_from_db = $t->tx->res->json; - -# Extract the id from the response. -my $set_id = $t->tx->res->json('/2/set_id'); - -$t->get_ok("/webwork3/api/courses/4/sets/$set_id")->status_is(200)->content_type_is('application/json;charset=UTF-8') - ->json_is('/set_name' => $hw_sets[2]->{set_name})->json_is('/set_type' => $hw_sets[2]->{set_type}) - ->json_is('/set_dates/open' => $hw_sets[2]->{set_dates}->{open}); - -# Create a new problem set -my $new_set = { - set_name => 'HW #9', - set_dates => { - open => 100, - due => 500, - answer => 500 - }, -}; -$t->post_ok('/webwork3/api/courses/4/sets' => json => $new_set)->content_type_is('application/json;charset=UTF-8') - ->json_is('/set_name' => 'HW #9')->json_is('/set_type' => 'HW')->json_is('/set_dates/answer' => 500); + my @arith_hw = grep { $_->{course_name} eq 'Arithmetic' } @hw_sets; -my $new_set_id = $t->tx->res->json('/set_id'); + $t->get_ok('/webwork3/api/courses/4/sets')->status_is(200)->content_type_is('application/json;charset=UTF-8') + ->json_is('/1/set_name' => $arith_hw[1]->{set_name}) + ->json_is('/1/set_dates/open' => $arith_hw[1]->{set_dates}{open}); -# Check that set_visible is a JSON boolean -my $set_visible = $t->tx->res->json('/set_visible'); -ok(!$set_visible, 'testing that set_visible is falsy'); -is($set_visible, false, 'Test that set_visible compares to Mojo::JSON::false'); -ok(JSON::PP::is_bool($set_visible), 'Test that set_visible is a JSON boolean'); -ok(JSON::PP::is_bool($set_visible) && !$set_visible, 'Test that set_visible is a JSON::false'); + my $arith_hw_from_db = $t->tx->res->json; -# Update the HW set. -$t->put_ok( - "/webwork3/api/courses/4/sets/$new_set_id" => json => { - set_name => 'HW #11', - set_visible => true - } -)->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => 'HW #11'); - -$set_visible = $t->tx->res->json('/set_visible'); -ok($set_visible, 'testing that set_visible is truthy'); -is($set_visible, true, 'Test that set_visible compares to Mojo::JSON::true'); -ok(JSON::PP::is_bool($set_visible), 'Test that set_visible is a JSON boolean'); -ok(JSON::PP::is_bool($set_visible) && $set_visible, 'Test that set_visible is a JSON:: true'); - -# get the first hw set in course: -my $hw1 = $arith_hw_from_db->[0]; - -# Test that booleans are returned correctly. -my $enabled = $hw1->{set_dates}->{enable_reduced_scoring}; -ok($enabled, 'testing that enabled_reduced_scoring compares to 1.'); -is($enabled, true, 'testing that enabled_reduced_scoring compares to Mojo::JSON::true'); -ok(JSON::PP::is_bool($enabled), 'testing that enabled_reduced_scoring is a Mojo::JSON::true or Mojo::JSON::false'); -ok(JSON::PP::is_bool($enabled) && $enabled, 'testing that enabled_reduced_scoring is a Mojo::JSON::true'); - -# Check that updating a boolean parameter is working: -$t->put_ok( - "/webwork3/api/courses/4/sets/$new_set_id" => json => { - set_params => { hide_hint => false } - } -)->content_type_is('application/json;charset=UTF-8'); - -my $hw2 = $t->tx->res->json; -my $hide_hint = $hw2->{set_params}->{hide_hint}; -ok(!$hide_hint, 'testing that hide_hint is falsy.'); -is($hide_hint, false, 'testing that hide_hint compares to Mojo::JSON::false'); -ok(JSON::PP::is_bool($hide_hint), 'testing that hide_hint is a Mojo::JSON::true or Mojo::JSON::false'); -ok(JSON::PP::is_bool($hide_hint) && !$hide_hint, 'testing that hide_hint is a Mojo::JSON::false'); - -# Test for exceptions - -# Try to get a set that is not in a course -$t->get_ok('/webwork3/api/courses/4/sets/99')->content_type_is('application/json;charset=UTF-8') - ->json_is('/exception' => 'DB::Exception::SetNotInCourse'); - -# Try to update a set not in a course -$t->put_ok('/webwork3/api/courses/4/sets/99' => json => { set_name => 'HW #99' }) - ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::SetNotInCourse'); - -# Try to add a set without a setname. -my $another_new_set = { name => 'this is the wrong field' }; -$t->post_ok('/webwork3/api/courses/4/sets' => json => $another_new_set) - ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::ParametersNeeded'); - -# Some cleanup to restore the databse -# Delete an existing set. -$t->delete_ok("/webwork3/api/courses/4/sets/$new_set_id")->content_type_is('application/json;charset=UTF-8') - ->json_is('/set_name' => 'HW #11'); + # Extract the id from the response. + my $set_id = $t->tx->res->json('/2/set_id'); + + $t->get_ok("/webwork3/api/courses/4/sets/$set_id")->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => $hw_sets[2]->{set_name}) + ->json_is('/set_type' => $hw_sets[2]->{set_type})->json_is('/set_dates/open' => $hw_sets[2]->{set_dates}{open}); + + # Create a new problem set + my $new_set = { set_name => 'HW #9', set_dates => { open => 100, due => 500, answer => 500 } }; + + $t->post_ok('/webwork3/api/courses/4/sets' => json => $new_set) + ->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => 'HW #9') + ->json_is('/set_type' => 'HW')->json_is('/set_dates/answer' => 500); + + my $new_set_id = $t->tx->res->json('/set_id'); + + # Check that set_visible is a JSON boolean + my $set_visible = $t->tx->res->json('/set_visible'); + ok(!$set_visible, 'testing that set_visible is falsy'); + is($set_visible, false, 'Test that set_visible compares to Mojo::JSON::false'); + ok(JSON::PP::is_bool($set_visible), 'Test that set_visible is a JSON boolean'); + ok(JSON::PP::is_bool($set_visible) && !$set_visible, 'Test that set_visible is a JSON::false'); + + # Update the HW set. + $t->put_ok("/webwork3/api/courses/4/sets/$new_set_id" => json => { set_name => 'HW #11', set_visible => true }) + ->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => 'HW #11'); + + $set_visible = $t->tx->res->json('/set_visible'); + ok($set_visible, 'testing that set_visible is truthy'); + is($set_visible, true, 'Test that set_visible compares to Mojo::JSON::true'); + ok(JSON::PP::is_bool($set_visible), 'Test that set_visible is a JSON boolean'); + ok(JSON::PP::is_bool($set_visible) && $set_visible, 'Test that set_visible is a JSON:: true'); + + # get the first hw set in course: + my $hw1 = $arith_hw_from_db->[0]; + + # Test that booleans are returned correctly. + my $enabled = $hw1->{set_dates}{enable_reduced_scoring}; + ok($enabled, 'testing that enabled_reduced_scoring compares to 1.'); + is($enabled, true, 'testing that enabled_reduced_scoring compares to Mojo::JSON::true'); + ok(JSON::PP::is_bool($enabled), + 'testing that enabled_reduced_scoring is a Mojo::JSON::true or Mojo::JSON::false'); + ok(JSON::PP::is_bool($enabled) && $enabled, 'testing that enabled_reduced_scoring is a Mojo::JSON::true'); + + # Check that updating a boolean parameter is working: + $t->put_ok("/webwork3/api/courses/4/sets/$new_set_id" => json => { set_params => { hide_hint => false } }) + ->content_type_is('application/json;charset=UTF-8'); + + my $hw2 = $t->tx->res->json; + my $hide_hint = $hw2->{set_params}{hide_hint}; + ok(!$hide_hint, 'testing that hide_hint is falsy.'); + is($hide_hint, false, 'testing that hide_hint compares to Mojo::JSON::false'); + ok(JSON::PP::is_bool($hide_hint), 'testing that hide_hint is a Mojo::JSON::true or Mojo::JSON::false'); + ok(JSON::PP::is_bool($hide_hint) && !$hide_hint, 'testing that hide_hint is a Mojo::JSON::false'); + + # Test for exceptions + + # Try to get a set that is not in a course + $t->get_ok('/webwork3/api/courses/4/sets/99')->content_type_is('application/json;charset=UTF-8') + ->json_is('/exception' => 'DB::Exception::SetNotInCourse'); + + # Try to update a set not in a course + $t->put_ok('/webwork3/api/courses/4/sets/99' => json => { set_name => 'HW #99' }) + ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::SetNotInCourse'); + + # Try to add a set without a setname. + my $another_new_set = { name => 'this is the wrong field' }; + $t->post_ok('/webwork3/api/courses/4/sets' => json => $another_new_set) + ->content_type_is('application/json;charset=UTF-8')->json_is('/exception' => 'DB::Exception::ParametersNeeded'); + + # Some cleanup to restore the databse + # Delete an existing set. + $t->delete_ok("/webwork3/api/courses/4/sets/$new_set_id")->content_type_is('application/json;charset=UTF-8') + ->json_is('/set_name' => 'HW #11'); +}; done_testing; diff --git a/t/mojolicious/006_quizzes.t b/t/mojolicious/006_quizzes.t index 0a31ff27..a10403eb 100644 --- a/t/mojolicious/006_quizzes.t +++ b/t/mojolicious/006_quizzes.t @@ -1,132 +1,100 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - use Test2::V0; +use Mojo::Base -signatures; use Test2::MojoX; +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json/; use Mojo::JSON qw/true false/; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - -use DB::Schema; -use Clone qw/clone/; -use YAML::XS qw/LoadFile/; -use TestUtils qw/loadCSV/; - -# Test the api with common 'courses/sets' routes for quizzes. - -# Load the config file. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = clone(LoadFile($config_file)); - -# Connect to the database. -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -my $t = Test2::MojoX->new(WeBWorK3 => $config); - -# Login as an user with instructor privileges in a course (Arithmetic; course_id: 4) -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) - ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); - -# Load the quizzes. -my @quizzes = loadCSV( - "$main::ww3_dir/t/db/sample_data/quizzes.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => ['timed'], - param_non_neg_int_fields => ['quiz_duration'] - } -); -for my $quiz (@quizzes) { - $quiz->{set_type} = "QUIZ"; - $quiz->{set_params} = {} unless defined($quiz->{set_params}); -} - -# Get all problem_sets -$t->get_ok('/webwork3/api/courses/4/sets')->content_type_is('application/json;charset=UTF-8'); -my $all_problem_sets = $t->tx->res->json; - -# find the first quiz in the course. - -my $quiz1 = (grep { $_->{set_type} eq 'QUIZ' } @$all_problem_sets)[0]; - -# test some things about this quiz -$t->get_ok("/webwork3/api/courses/4/sets/$quiz1->{set_id}")->content_type_is('application/json;charset=UTF-8') - ->json_is('/set_name' => 'Quiz #1'); - -# Test that booleans are returned correctly. - -$quiz1 = $t->tx->res->json; -my $timed = $quiz1->{set_params}->{timed}; -ok($timed, 'testing that timed compares to 1.'); -is($timed, true, 'testing that timed compares to true'); -ok(JSON::PP::is_bool($timed), 'testing that timed is a true or false'); -ok(JSON::PP::is_bool($timed) && $timed, 'testing that timed is a true'); - -# Make a new quiz - -my $new_quiz_params = { - set_name => 'Quiz #20', - set_type => 'QUIZ', - set_params => { - timed => true, - quiz_duration => 30, - problem_randorder => true - }, - set_dates => { - open => 100, - due => 200, - answer => 300 +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use DBSubtest qw/mojoDBSubtest/; +use BuildDB qw/loadPermissions addCourses addUsers addSets/; + +mojoDBSubtest 'course/sets routes for quizzes' => sub ($t, $schema) { + # Add the neccessary sample data for the test. + loadPermissions($schema, $t->app->home); + addCourses($schema, $t->app->home); + addUsers($schema, $t->app->home); + addSets($schema, $t->app->home); + + # Login as an user with instructor privileges in a course (Arithmetic; course_id: 4) + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); + + # Load quiz sets from JSON file. + my $course_quizzes = decode_json($t->app->home->child('t/db/sample_data/quizzes.json')->slurp); + my @quizzes; + for my $course_data (@$course_quizzes) { + for my $quiz (@{ $course_data->{sets} }) { + $quiz->{set_type} = 'QUIZ'; + $quiz->{course_name} = $course_data->{course_name}; + push(@quizzes, $quiz); + } } -}; -$t->post_ok('/webwork3/api/courses/4/sets' => json => $new_quiz_params) - ->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => 'Quiz #20') - ->json_is('/set_type' => 'QUIZ'); -my $returned_quiz = $t->tx->res->json; + # Get all problem_sets + $t->get_ok('/webwork3/api/courses/4/sets')->content_type_is('application/json;charset=UTF-8'); + my $all_problem_sets = $t->tx->res->json; -my $new_quiz = $t->tx->res->json; -my $problem_randorder = $new_quiz->{set_params}->{problem_randorder}; -ok($problem_randorder, 'testing that problem_randorder compares to 1.'); -is($problem_randorder, true, 'testing that problem_randorder compares to true'); -ok(JSON::PP::is_bool($problem_randorder), 'testing that problem_randorder is a true or false'); -ok(JSON::PP::is_bool($problem_randorder) && $problem_randorder, 'testing that problem_randorder is a true'); + # find the first quiz in the course. -# Check that updating a boolean parameter is working: + my $quiz1 = (grep { $_->{set_type} eq 'QUIZ' } @$all_problem_sets)[0]; -$t->put_ok( - "/webwork3/api/courses/4/sets/$returned_quiz->{set_id}" => json => { - set_params => { - problem_randorder => false - } - } -)->status_is(200); + # test some things about this quiz + $t->get_ok("/webwork3/api/courses/4/sets/$quiz1->{set_id}")->content_type_is('application/json;charset=UTF-8') + ->json_is('/set_name' => 'Quiz #1'); + + # Test that booleans are returned correctly. + + $quiz1 = $t->tx->res->json; + my $timed = $quiz1->{set_params}->{timed}; + ok($timed, 'testing that timed compares to 1.'); + is($timed, true, 'testing that timed compares to true'); + ok(JSON::PP::is_bool($timed), 'testing that timed is a true or false'); + ok(JSON::PP::is_bool($timed) && $timed, 'testing that timed is a true'); -my $updated_quiz = $t->tx->res->json; + # Make a new quiz -$problem_randorder = $updated_quiz->{set_params}->{problem_randorder}; -ok(!$problem_randorder, 'testing that hide_hint is falsy.'); -is($problem_randorder, false, 'testing that problem_randorder compares to false'); -ok(JSON::PP::is_bool($problem_randorder), 'testing that problem_randorder is a true or false'); -ok(JSON::PP::is_bool($problem_randorder) && !$problem_randorder, 'testing that problem_randorder is a false'); + my $new_quiz_params = { + set_name => 'Quiz #20', + set_type => 'QUIZ', + set_params => { timed => true, quiz_duration => 30, problem_randorder => true }, + set_dates => { open => 100, due => 200, answer => 300 } + }; -# delete the added quiz -$t->delete_ok("/webwork3/api/courses/4/sets/$returned_quiz->{set_id}") - ->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => 'Quiz #20') - ->json_is('/set_type' => 'QUIZ'); + $t->post_ok('/webwork3/api/courses/4/sets' => json => $new_quiz_params) + ->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => 'Quiz #20') + ->json_is('/set_type' => 'QUIZ'); + my $returned_quiz = $t->tx->res->json; + + my $new_quiz = $t->tx->res->json; + my $problem_randorder = $new_quiz->{set_params}->{problem_randorder}; + ok($problem_randorder, 'testing that problem_randorder compares to 1.'); + is($problem_randorder, true, 'testing that problem_randorder compares to true'); + ok(JSON::PP::is_bool($problem_randorder), 'testing that problem_randorder is a true or false'); + ok(JSON::PP::is_bool($problem_randorder) && $problem_randorder, 'testing that problem_randorder is a true'); + + # Check that updating a boolean parameter is working: + + $t->put_ok("/webwork3/api/courses/4/sets/$returned_quiz->{set_id}" => json => + { set_params => { problem_randorder => false } })->status_is(200); + + my $updated_quiz = $t->tx->res->json; + + $problem_randorder = $updated_quiz->{set_params}->{problem_randorder}; + ok(!$problem_randorder, 'testing that hide_hint is falsy.'); + is($problem_randorder, false, 'testing that problem_randorder compares to false'); + ok(JSON::PP::is_bool($problem_randorder), 'testing that problem_randorder is a true or false'); + ok(JSON::PP::is_bool($problem_randorder) && !$problem_randorder, 'testing that problem_randorder is a false'); + + # delete the added quiz + $t->delete_ok("/webwork3/api/courses/4/sets/$returned_quiz->{set_id}") + ->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => 'Quiz #20') + ->json_is('/set_type' => 'QUIZ'); +}; done_testing(); diff --git a/t/mojolicious/007_review_sets.t b/t/mojolicious/007_review_sets.t index 86b7dc88..56661c97 100644 --- a/t/mojolicious/007_review_sets.t +++ b/t/mojolicious/007_review_sets.t @@ -1,128 +1,97 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - use Test2::V0; +use Mojo::Base -signatures; use Test2::MojoX; -use Mojo::JSON qw/true false/; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - -use DB::Schema; -use Clone qw/clone/; -use YAML::XS qw/LoadFile/; -use TestUtils qw/loadCSV/; - -# Test the api with common "users" routes. - -# Load the config file. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = clone(LoadFile($config_file)); - -# Connect to the database. -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -my $t = Test2::MojoX->new(WeBWorK3 => $config); - -# Login as an user with instructor privileges in a course (Arithmetic; course_id: 4) -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) - ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); - -# Load the review sets from the CSV file -my @review_sets = loadCSV( - "$main::ww3_dir/t/db/sample_data/review_sets.csv", - { - boolean_fields => ['set_visible'], - param_boolean_fields => ['can_retake'] +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json true false/; + +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use DBSubtest qw/mojoDBSubtest/; +use BuildDB qw/loadPermissions addCourses addUsers addSets/; + +mojoDBSubtest 'course/sets routes for review sets' => sub ($t, $schema) { + # Add the neccessary sample data for the test. + loadPermissions($schema, $t->app->home); + addCourses($schema, $t->app->home); + addUsers($schema, $t->app->home); + addSets($schema, $t->app->home); + + # Login as an user with instructor privileges in a course (Arithmetic; course_id: 4) + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); + + # Load review sets from JSON file. + my $course_review_sets = decode_json($t->app->home->child('t/db/sample_data/review_sets.json')->slurp); + my @review_sets; + for my $course_data (@$course_review_sets) { + for my $review_set (@{ $course_data->{sets} }) { + $review_set->{set_type} = 'REVIEW'; + $review_set->{course_name} = $course_data->{course_name}; + push(@review_sets, $review_set); + } } -); -for my $set (@review_sets) { - $set->{set_type} = 'REVIEW'; - $set->{set_params} = {} unless defined $set->{set_params}; - -} - -# Get all problem_sets -$t->get_ok('/webwork3/api/courses/4/sets')->content_type_is('application/json;charset=UTF-8'); -my $all_problem_sets = $t->tx->res->json; - -# find the first review set in the course. -my $review_set1 = (grep { $_->{set_type} eq 'REVIEW' } @$all_problem_sets)[0]; - -# test some things about this review set -$t->get_ok("/webwork3/api/courses/4/sets/$review_set1->{set_id}")->status_is(200) - ->json_is('/set_name' => $review_set1->{set_name}); + # Get all problem_sets + $t->get_ok('/webwork3/api/courses/4/sets')->content_type_is('application/json;charset=UTF-8'); + my $all_problem_sets = $t->tx->res->json; -# Test that booleans are returned correctly. + # find the first review set in the course. + my $review_set1 = (grep { $_->{set_type} eq 'REVIEW' } @$all_problem_sets)[0]; -$review_set1 = $t->tx->res->json; + # test some things about this review set -# The parameter can_retake should be false. -my $can_retake = $review_set1->{set_params}->{can_retake}; -ok(!$can_retake, 'testing that can_retake compares to 0.'); -is($can_retake, false, 'testing that can_retake compares to Mojo::JSON::false'); -ok(JSON::PP::is_bool($can_retake), 'testing that can_retake is a Mojo::JSON::true or Mojo::JSON::false'); -ok(JSON::PP::is_bool($can_retake) && !$can_retake, 'testing that can_retake is a Mojo::JSON::true'); + $t->get_ok("/webwork3/api/courses/4/sets/$review_set1->{set_id}")->status_is(200) + ->json_is('/set_name' => $review_set1->{set_name}); -# Make a new quiz + # Test that booleans are returned correctly. -my $new_review_set_params = { - set_name => 'Review #20', - set_type => 'REVIEW', - set_params => { - can_retake => true, - }, - set_dates => { - open => 100, - closed => 200 - } -}; - -$t->post_ok('/webwork3/api/courses/4/sets' => json => $new_review_set_params) - ->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => 'Review #20') - ->json_is('/set_type' => 'REVIEW'); + $review_set1 = $t->tx->res->json; -$review_set1 = $t->tx->res->json; -$can_retake = $review_set1->{set_params}->{can_retake}; -ok($can_retake, 'testing that can_retake compares to 1.'); -is($can_retake, true, 'testing that can_retake compares to Mojo::JSON::true'); -ok(JSON::PP::is_bool($can_retake), 'testing that can_retake is a Mojo::JSON::true or Mojo::JSON::false'); -ok(JSON::PP::is_bool($can_retake) && $can_retake, 'testing that can_retake is a Mojo::JSON::true'); + # The parameter can_retake should be false. + my $can_retake = $review_set1->{set_params}->{can_retake}; + ok(!$can_retake, 'testing that can_retake compares to 0.'); + is($can_retake, false, 'testing that can_retake compares to Mojo::JSON::false'); + ok(JSON::PP::is_bool($can_retake), 'testing that can_retake is a Mojo::JSON::true or Mojo::JSON::false'); + ok(JSON::PP::is_bool($can_retake) && !$can_retake, 'testing that can_retake is a Mojo::JSON::true'); -# Check that updating a boolean parameter is working: - -$t->put_ok( - "/webwork3/api/courses/4/sets/$review_set1->{set_id}" => json => { - set_params => { - can_retake => false + # Make a new review set. + $t->post_ok( + '/webwork3/api/courses/4/sets' => json => { + set_name => 'Review #20', + set_type => 'REVIEW', + set_params => { can_retake => true, }, + set_dates => { open => 100, closed => 200 } } - } -)->content_type_is('application/json;charset=UTF-8'); - -my $updated_set = $t->tx->res->json; -$can_retake = $updated_set->{set_params}->{can_retake}; -ok(!$can_retake, 'testing that can_retake is falsy.'); -is($can_retake, false, 'testing that can_retake compares to Mojo::JSON::false'); -ok(JSON::PP::is_bool($can_retake), 'testing that can_retake is a Mojo::JSON::true or Mojo::JSON::false'); -ok(JSON::PP::is_bool($can_retake) && !$can_retake, 'testing that can_retake is a Mojo::JSON::false'); - -# delete the added review set -$t->delete_ok("/webwork3/api/courses/4/sets/$review_set1->{set_id}")->content_type_is('application/json;charset=UTF-8') - ->json_is('/set_type' => 'REVIEW')->json_is('/set_name' => 'Review #20'); + )->content_type_is('application/json;charset=UTF-8')->json_is('/set_name' => 'Review #20') + ->json_is('/set_type' => 'REVIEW'); + + $review_set1 = $t->tx->res->json; + $can_retake = $review_set1->{set_params}->{can_retake}; + ok($can_retake, 'testing that can_retake compares to 1.'); + is($can_retake, true, 'testing that can_retake compares to Mojo::JSON::true'); + ok(JSON::PP::is_bool($can_retake), 'testing that can_retake is a Mojo::JSON::true or Mojo::JSON::false'); + ok(JSON::PP::is_bool($can_retake) && $can_retake, 'testing that can_retake is a Mojo::JSON::true'); + + # Check that updating a boolean parameter is working: + $t->put_ok( + "/webwork3/api/courses/4/sets/$review_set1->{set_id}" => json => { set_params => { can_retake => false } }) + ->content_type_is('application/json;charset=UTF-8'); + + my $updated_set = $t->tx->res->json; + $can_retake = $updated_set->{set_params}->{can_retake}; + ok(!$can_retake, 'testing that can_retake is falsy.'); + is($can_retake, false, 'testing that can_retake compares to Mojo::JSON::false'); + ok(JSON::PP::is_bool($can_retake), 'testing that can_retake is a Mojo::JSON::true or Mojo::JSON::false'); + ok(JSON::PP::is_bool($can_retake) && !$can_retake, 'testing that can_retake is a Mojo::JSON::false'); + + # delete the added review set + $t->delete_ok("/webwork3/api/courses/4/sets/$review_set1->{set_id}") + ->content_type_is('application/json;charset=UTF-8')->json_is('/set_type' => 'REVIEW') + ->json_is('/set_name' => 'Review #20'); +}; done_testing(); diff --git a/t/mojolicious/008_problems.t b/t/mojolicious/008_problems.t index 88620c59..0d2cba52 100644 --- a/t/mojolicious/008_problems.t +++ b/t/mojolicious/008_problems.t @@ -1,181 +1,158 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - use Test2::V0; +use Mojo::Base -signatures; use Test2::MojoX; -use Mojo::JSON qw/true false/; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - -use DB::Schema; -use Clone qw/clone/; -use YAML::XS qw/LoadFile/; - -use TestUtils qw/loadCSV removeIDs/; - -# Test the api with common 'courses/sets' routes. - -# Load the config file. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = clone(LoadFile($config_file)); - -# Connect to the database. -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); -my $t = Test2::MojoX->new(WeBWorK3 => $config); - -# First run tests as logged in as an instructor -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) - ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); - -# Load all problems from the CVS files. -my @problems_from_csv = loadCSV( - "$main::ww3_dir/t/db/sample_data/problems.csv", - { - non_neg_int_fields => ['problem_number'], - param_non_neg_int_fields => ['library_id'] - } -); - -# Filter out Arithmetic problems -my @arith_problems = grep { $_->{course_name} eq 'Arithmetic' } @problems_from_csv; - -# Filter out 'HW #1' -my @arith_problems1 = grep { $_->{set_name} eq 'HW #1' } @arith_problems; - -for my $problem (@arith_problems) { - for my $key (qw/set_name course_name/) { - delete $problem->{$key}; +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json true false/; + +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use DBSubtest qw/mojoDBSubtest/; +use BuildDB qw/loadPermissions addCourses addUsers addSets addProblems/; +use TestUtils qw/removeIDs/; + +mojoDBSubtest 'course/sets/problems routes' => sub ($t, $schema) { + # Add the neccessary sample data for the test. + loadPermissions($schema, $t->app->home); + addCourses($schema, $t->app->home); + addUsers($schema, $t->app->home); + addSets($schema, $t->app->home); + addProblems($schema, $t->app->home); + + # First run tests as logged in as an instructor + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); + + # Load all problems from the the JSON file. + my $course_set_problems = decode_json($t->app->home->child('t/db/sample_data/problems.json')->slurp); + my @problems_from_json; + my @arith_problems; + my @arith_problems1; + for my $course_info (@$course_set_problems) { + for my $set_info (@{ $course_info->{sets} }) { + for (@{ $set_info->{problems} }) { + push(@problems_from_json, { %$_, set_name => $set_info->{set_name} }); + + # Separate out arithmetic problems. + push(@arith_problems, $_) if $course_info->{course_name} eq 'Arithmetic'; + + # Separate out arithmetic 'HW #1' problems. + push(@arith_problems1, $_) + if $course_info->{course_name} eq 'Arithmetic' && $set_info->{set_name} eq 'HW #1'; + } + } } -} -# Get all of the homework sets in Arithmetic course (needed later) + # Get all of the homework sets in Arithmetic course (needed later) + $t->get_ok('/webwork3/api/courses/4/sets')->status_is(200)->content_type_is('application/json;charset=UTF-8'); -$t->get_ok('/webwork3/api/courses/4/sets')->status_is(200)->content_type_is('application/json;charset=UTF-8'); + my $sets = $t->tx->res->json; -my $sets = $t->tx->res->json; + my $hw1 = (grep { $_->{set_name} eq 'HW #1' } @$sets)[0]; -my $hw1 = (grep { $_->{set_name} eq 'HW #1' } @$sets)[0]; + # Get all Arithmetic problems (course_id: 4) -# Get all Arithmetic problems (course_id: 4) + $t->get_ok('/webwork3/api/courses/4/problems')->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); -$t->get_ok('/webwork3/api/courses/4/problems')->status_is(200)->content_type_is('application/json;charset=UTF-8'); + my $problems_from_db = $t->tx->res->json; + for my $problem (@$problems_from_db) { removeIDs($problem); } -my $problems_from_db = $t->tx->res->json; -for my $problem (@$problems_from_db) { removeIDs($problem); } + is($problems_from_db, \@arith_problems, 'getGlobalProblems: get all problems'); -is($problems_from_db, \@arith_problems, 'getGlobalProblems: get all problems'); + # Add a new problem to a set. -# Add a new problem to a set. - -$t->post_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/problems" => json => { - problem_params => { - file_path => 'this/is/not/a/real/path.pg' + $t->post_ok( + "/webwork3/api/courses/4/sets/$hw1->{set_id}/problems" => json => { + problem_params => { file_path => 'this/is/not/a/real/path.pg' } } - } -)->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/set_id' => $hw1->{set_id}); + )->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/set_id' => $hw1->{set_id}); -my $new_problem = $t->tx->res->json; + my $new_problem = $t->tx->res->json; -# Get a single problem + # Get a single problem -$t->get_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}")->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/set_id' => $new_problem->{set_id}) - ->json_is('/problem_number' => $new_problem->{problem_number}); + $t->get_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}") + ->status_is(200)->content_type_is('application/json;charset=UTF-8') + ->json_is('/set_id' => $new_problem->{set_id})->json_is('/problem_number' => $new_problem->{problem_number}); -# Update the problem -$t->put_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}" => json => { - problem_params => { - weight => 3 + # Update the problem + $t->put_ok( + "/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}" => json => { + problem_params => { weight => 3 } } - } -)->status_is(200)->content_type_is('application/json;charset=UTF-8') - ->json_is('/problem_number' => $new_problem->{problem_number})->json_is('/problem_params/weight' => 3); + )->status_is(200)->content_type_is('application/json;charset=UTF-8') + ->json_is('/problem_number' => $new_problem->{problem_number})->json_is('/problem_params/weight' => 3); -# Make sure that a student cannot access the global problem routes -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); -$t->post_ok('/webwork3/api/login' => json => { username => 'ralph', password => 'ralph' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) - ->json_is('/user/username' => 'ralph')->json_is('/user/is_admin' => false); + # Make sure that a student cannot access the global problem routes + $t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); + $t->post_ok('/webwork3/api/login' => json => { username => 'ralph', password => 'ralph' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'ralph')->json_is('/user/is_admin' => false); -my $logged_in_user = $t->tx->res->json('/user'); + my $logged_in_user = $t->tx->res->json('/user'); -# Make sure ralph is enrolled in the Arithmetic course by getting the course users -$t->get_ok("/webwork3/api/users/$logged_in_user->{user_id}/courses"); + # Make sure ralph is enrolled in the Arithmetic course by getting the course users + $t->get_ok("/webwork3/api/users/$logged_in_user->{user_id}/courses"); -my $user_courses = $t->tx->res->json; -is(scalar(grep { $_->{course_name} eq 'Arithmetic' } @$user_courses), 1, 'The user ralph is enrolled in the course.'); + my $user_courses = $t->tx->res->json; + is(scalar(grep { $_->{course_name} eq 'Arithmetic' } @$user_courses), + 1, 'The user ralph is enrolled in the course.'); -# a student should have access to getting global problems -$t->get_ok('/webwork3/api/courses/4/problems')->status_is(200)->content_type_is('application/json;charset=UTF-8'); + # a student should have access to getting global problems + $t->get_ok('/webwork3/api/courses/4/problems')->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); -my $all_problems = $t->tx->res->json; + my $all_problems = $t->tx->res->json; -# Get a single problem -my $set_problem_id = $all_problems->[ scalar(@$all_problems) - 1 ]->{set_problem_id}; + # Get a single problem + my $set_problem_id = $all_problems->[ scalar(@$all_problems) - 1 ]->{set_problem_id}; -$t->get_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$set_problem_id")->status_is(200) - ->content_type_is('application/json;charset=UTF-8'); + $t->get_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$set_problem_id")->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); -# But not to adding, updating or deleting a problem -$t->post_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/problems" => json => { - problem_params => { - file_path => 'this/is/not/a/real/path.pg' + # But not to adding, updating or deleting a problem + $t->post_ok( + "/webwork3/api/courses/4/sets/$hw1->{set_id}/problems" => json => { + problem_params => { file_path => 'this/is/not/a/real/path.pg' } } - } -)->status_is(403)->content_type_is('application/json;charset=UTF-8'); + )->status_is(403)->content_type_is('application/json;charset=UTF-8'); -$t->put_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}" => json => { - problem_params => { - weight => 3 - } - } -)->status_is(403)->content_type_is('application/json;charset=UTF-8'); + $t->put_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}" => json => + { problem_params => { weight => 3 } })->status_is(403)->content_type_is('application/json;charset=UTF-8'); -$t->delete_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}")->status_is(403) - ->content_type_is('application/json;charset=UTF-8'); + $t->delete_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}") + ->status_is(403)->content_type_is('application/json;charset=UTF-8'); -# Make sure that a student not enrolled in the course has access to getting global problems -# for that course. -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); -$t->post_ok('/webwork3/api/login' => json => { username => 'ned', password => 'ned' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) - ->json_is('/user/username' => 'ned')->json_is('/user/is_admin' => false); + # Make sure that a student not enrolled in the course has access to getting global problems + # for that course. + $t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); + $t->post_ok('/webwork3/api/login' => json => { username => 'ned', password => 'ned' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'ned')->json_is('/user/is_admin' => false); -$logged_in_user = $t->tx->res->json('/user'); + $logged_in_user = $t->tx->res->json('/user'); -# check that ned is not in the course -$t->get_ok("/webwork3/api/users/$logged_in_user->{user_id}/courses"); + # check that ned is not in the course + $t->get_ok("/webwork3/api/users/$logged_in_user->{user_id}/courses"); -$user_courses = $t->tx->res->json; + $user_courses = $t->tx->res->json; -is(scalar(grep { $_->{course_name} eq 'Arithmetic' } @$user_courses), 0, 'The user ned is not enrolled in the course.'); + is(scalar(grep { $_->{course_name} eq 'Arithmetic' } @$user_courses), + 0, 'The user ned is not enrolled in the course.'); -$t->get_ok('/webwork3/api/courses/4/problems')->status_is(403)->content_type_is('application/json;charset=UTF-8'); + $t->get_ok('/webwork3/api/courses/4/problems')->status_is(403) + ->content_type_is('application/json;charset=UTF-8'); -# Finally, delete the new problem to restore the db to it's pretest state. -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); -$t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200); + # Finally, delete the new problem to restore the db to it's pretest state. + $t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); + $t->post_ok('/webwork3/api/login' => json => { username => 'admin', password => 'admin' })->status_is(200); -$t->delete_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}")->status_is(200) - ->content_type_is('application/json;charset=UTF-8'); + $t->delete_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}") + ->status_is(200)->content_type_is('application/json;charset=UTF-8'); +}; done_testing; diff --git a/t/mojolicious/009_user_problems.t b/t/mojolicious/009_user_problems.t index 6af18b9e..2ba938f3 100644 --- a/t/mojolicious/009_user_problems.t +++ b/t/mojolicious/009_user_problems.t @@ -1,245 +1,219 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - use Test2::V0; +use Mojo::Base -signatures; use Test2::MojoX; -use Mojo::JSON qw/true false/; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - -use DB::Schema; -use Clone qw/clone/; -use YAML::XS qw/LoadFile/; - -use TestUtils qw/loadCSV removeIDs/; - -# Test the api with common 'courses/sets' routes. - -# Load the config file. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = clone(LoadFile($config_file)); - -# Connect to the database. -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); -my $t = Test2::MojoX->new(WeBWorK3 => $config); - -# First run tests as logged in as an instructor -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) - ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); - -# Load all problems from the CVS files. -my @problems_from_csv = loadCSV( - "$main::ww3_dir/t/db/sample_data/problems.csv", - { - non_neg_int_fields => ['problem_number'], - param_non_neg_int_fields => ['library_id'] +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json true false/; + +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use DBSubtest qw/mojoDBSubtest/; +use BuildDB qw/loadPermissions addCourses addUsers addSets addProblems addUserSets addUserProblems/; +use TestUtils qw/removeIDs/; + +mojoDBSubtest 'user problem routes for review sets' => sub ($t, $schema) { + # Add the neccessary sample data for the test. + loadPermissions($schema, $t->app->home); + addCourses($schema, $t->app->home); + addUsers($schema, $t->app->home); + addSets($schema, $t->app->home); + addProblems($schema, $t->app->home); + addUserSets($schema, $t->app->home); + addUserProblems($schema, $t->app->home); + + # First run tests as logged in as an instructor + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); + + # Load arithmetic problems from the the JSON file. + my @arith_problems; + for my $course_info (@{ decode_json($t->app->home->child('t/db/sample_data/problems.json')->slurp) }) { + next unless $course_info->{course_name} eq 'Arithmetic'; + + for my $set_info (@{ $course_info->{sets} }) { + for (@{ $set_info->{problems} }) { + push(@arith_problems, $_); + } + } } -); -my @user_problems_from_csv = loadCSV( - "$main::ww3_dir/t/db/sample_data/user_problems.csv", - { - non_neg_int_fields => [ 'problem_number', 'seed' ] + + # Load arithmetic user problems from the JSON file. + my @arith_user_problems; + for my $course_info (@{ decode_json($t->app->home->child('t/db/sample_data/user_problems.json')->slurp) }) { + next unless $course_info->{course_name} eq 'Arithmetic'; + + for my $set_info (@{ $course_info->{sets} }) { + for my $problem_info (@{ $set_info->{problems} }) { + for my $user_info (@{ $problem_info->{users} }) { + $user_info->{user_problem}{course_name} = $course_info->{course_name}; + $user_info->{user_problem}{set_name} = $set_info->{set_name}; + $user_info->{user_problem}{problem_number} = $problem_info->{problem_number}; + $user_info->{user_problem}{username} = $user_info->{username}; + $user_info->{user_problem}{status} //= 1; + $user_info->{user_problem}{problem_params} //= {}; + $user_info->{user_problem}{problem_version} //= 1; + + push(@arith_user_problems, $user_info->{user_problem}); + } + } + } } -); + @arith_user_problems = + sort { $a->{username} cmp $b->{username} || $a->{problem_number} <=> $b->{problem_number} } + @arith_user_problems; -# Filter out Arithmetic problems -my @arith_problems = grep { $_->{course_name} eq 'Arithmetic' } @problems_from_csv; -my @arith_user_problems = grep { $_->{course_name} eq 'Arithmetic' } @user_problems_from_csv; + # Get all of the users and homework sets in Arithmetic course (needed later) + $t->get_ok('/webwork3/api/courses/4/global-courseusers')->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); + my $all_users = $t->tx->res->json; -# Filter out 'HW #1' -my @arith_problems1 = grep { $_->{set_name} eq 'HW #1' } @arith_problems; -my @arith_user_problems1 = grep { $_->{set_name} eq 'HW #1' } @arith_user_problems; + $t->get_ok('/webwork3/api/courses/4/sets')->status_is(200)->content_type_is('application/json;charset=UTF-8'); + my $sets = $t->tx->res->json; + my $hw1 = (grep { $_->{set_name} eq 'HW #1' } @$sets)[0]; -for my $problem (@arith_problems1) { - for my $key (qw/set_name course_name/) { - delete $problem->{$key}; - } -} + # Get all Arithmetic problems (course_id: 4) -for my $problem (@arith_user_problems1) { - $problem->{problem_params} = {} unless defined($problem->{problem_params}); - $problem->{problem_version} = 1 unless defined($problem->{problem_version}); -} + $t->get_ok('/webwork3/api/courses/4/problems')->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); -# Get all of the users and homework sets in Arithmetic course (needed later) + my $problems_from_db = $t->tx->res->json; + for my $problem (@$problems_from_db) { removeIDs($problem); } -$t->get_ok('/webwork3/api/courses/4/global-courseusers')->status_is(200) - ->content_type_is('application/json;charset=UTF-8'); -my $all_users = $t->tx->res->json; + is($problems_from_db, \@arith_problems, 'getGlobalProblems: get all problems'); -$t->get_ok('/webwork3/api/courses/4/sets')->status_is(200)->content_type_is('application/json;charset=UTF-8'); -my $sets = $t->tx->res->json; -my $hw1 = (grep { $_->{set_name} eq 'HW #1' } @$sets)[0]; + # Get all user problems for a single set. -# Get all Arithmetic problems (course_id: 4) + $t->get_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/user-problems")->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); -$t->get_ok('/webwork3/api/courses/4/problems')->status_is(200)->content_type_is('application/json;charset=UTF-8'); + my $user_problems_from_db = $t->tx->res->json; + for my $problem (@$user_problems_from_db) { removeIDs($problem); } -my $problems_from_db = $t->tx->res->json; -for my $problem (@$problems_from_db) { removeIDs($problem); } + # the status needs be returned to a numerical value. + $_->{status} += 0 for (@$user_problems_from_db); -is($problems_from_db, \@arith_problems, 'getGlobalProblems: get all problems'); + my @user_problems_from_db = + sort { $a->{username} cmp $b->{username} || $a->{problem_number} <=> $b->{problem_number} } + @$user_problems_from_db; -# Get all user problems for a single set. + is(\@user_problems_from_db, \@arith_user_problems, 'getUserProblems: get all problems for a set in a course.'); -$t->get_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/user-problems")->status_is(200) - ->content_type_is('application/json;charset=UTF-8'); + # Get all user problems in a course for a single user. Find user 'ralph' -my $user_problems_from_db = $t->tx->res->json; -for my $problem (@$user_problems_from_db) { removeIDs($problem); } + my $ralph = (grep { $_->{username} eq 'ralph' } @$all_users)[0]; -# the status needs be returned to a numerical value. -$_->{status} += 0 for (@$user_problems_from_db); + $t->get_ok("/webwork3/api/courses/4/users/$ralph->{user_id}/problems"); + my $ralph_user_problems = $t->tx->res->json; -@arith_user_problems = - sort { $a->{username} cmp $b->{username} || $a->{problem_number} <=> $b->{problem_number} } @arith_user_problems; -my @user_problems_from_db = - sort { $a->{username} cmp $b->{username} || $a->{problem_number} <=> $b->{problem_number} } @$user_problems_from_db; + for my $problem (@$ralph_user_problems) { + removeIDs($problem); + } -is(\@user_problems_from_db, \@arith_user_problems, 'getUserProblems: get all problems for a set in a course.'); + my @ralph_user_problems_from_file = grep { $_->{username} eq 'ralph' } @arith_user_problems; + # For comparision make sure the loaded status are printed to 5 digits. + $_->{status} += 0 for (@$ralph_user_problems); -# Get all user problems in a course for a single user. Find user 'ralph' + is( + $ralph_user_problems, + \@ralph_user_problems_from_file, + 'getUserProblems: get all problems for a set in a course.' + ); -my $ralph = (grep { $_->{username} eq 'ralph' } @$all_users)[0]; + # New to make a new problem first -$t->get_ok("/webwork3/api/courses/4/users/$ralph->{user_id}/problems"); -my $ralph_user_problems = $t->tx->res->json; + $t->post_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems" => json => + { problem_params => { file_path => 'this/is/not/a/real/path.pg' } })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/set_id' => $hw1->{set_id}); -for my $problem (@$ralph_user_problems) { - removeIDs($problem); -} + my $new_problem = $t->tx->res->json; -my @ralph_user_problems_from_file = grep { $_->{username} eq 'ralph' } @arith_user_problems; -# For comparision make sure the loaded status are printed to 5 digits. -$_->{status} += 0 for (@$ralph_user_problems); + # Create a new user problem -is($ralph_user_problems, \@ralph_user_problems_from_file, 'getUserProblems: get all problems for a set in a course.'); + $t->post_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems" => json => + { seed => 5421, problem_number => $new_problem->{problem_number}, problem_params => { weight => 3 } }) + ->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/seed' => 5421) + ->json_is('/problem_params/weight' => 3); -# New to make a new problem first + my $new_user_problem = $t->tx->res->json; -$t->post_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/problems" => json => { - problem_params => { - file_path => 'this/is/not/a/real/path.pg' - } - } -)->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/set_id' => $hw1->{set_id}); + # get the user problem -my $new_problem = $t->tx->res->json; + $t->get_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/users/" + . "$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}")->status_is(200) + ->json_is('/seed' => 5421)->json_is('/problem_params/weight' => 3); -# Create a new user problem + # Update the user problem -$t->post_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems" => json => { - seed => 5421, - problem_number => $new_problem->{problem_number}, - problem_params => { - weight => 3 - } - } -)->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/seed' => 5421) - ->json_is('/problem_params/weight' => 3); - -my $new_user_problem = $t->tx->res->json; - -# get the user problem - -$t->get_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}") - ->status_is(200)->json_is('/seed' => 5421)->json_is('/problem_params/weight' => 3); - -# Update the user problem - -$t->put_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}" - => json => { seed => 789 })->status_is(200)->content_type_is('application/json;charset=UTF-8') - ->json_is('/seed' => 789); - -# Check that a student has the correct access -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); -$t->post_ok('/webwork3/api/login' => json => { username => 'ralph', password => 'ralph' })->status_is(200); - -# get all of ralph's problems -$t->get_ok("/webwork3/api/courses/4/users/$ralph->{user_id}/problems")->status_is(200); - -# get a single problem -$t->get_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}") - ->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/seed' => 789) - ->json_is('/problem_params/weight' => 3); - -# Update a problem -$t->put_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}" - => json => { status => 0.5 })->status_is(200)->content_type_is('application/json;charset=UTF-8') - ->json_is('/status' => 0.5); - -# A student shouldn't be able to create a new problem or delete -$t->post_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems" => json => { - seed => 5421, - problem_number => $new_problem->{problem_number}, - problem_params => { - weight => 3 - } - } -)->status_is(403); + $t->put_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/users/" + . "$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}" => json => { seed => 789 }) + ->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/seed' => 789); + + # Check that a student has the correct access + $t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); + $t->post_ok('/webwork3/api/login' => json => { username => 'ralph', password => 'ralph' })->status_is(200); + + # get all of ralph's problems + $t->get_ok("/webwork3/api/courses/4/users/$ralph->{user_id}/problems")->status_is(200); + + # get a single problem + $t->get_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/users/" + . "$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}")->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/seed' => 789) + ->json_is('/problem_params/weight' => 3); + + # Update a problem + $t->put_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/users/" + . "$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}" => json => { status => 0.5 }) + ->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/status' => 0.5); + + # A student shouldn't be able to create a new problem or delete + $t->post_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems" => json => + { seed => 5421, problem_number => $new_problem->{problem_number}, problem_params => { weight => 3 } }) + ->status_is(403); -$t->delete_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}") - ->status_is(403); + $t->delete_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/users/" + . "$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}")->status_is(403); -# Make sure that a user that is in the course, cannot get or update a user problem that is not one's own. -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); -$t->post_ok('/webwork3/api/login' => json => { username => 'moe', password => 'moe' })->status_is(200); + # Make sure that a user that is in the course, cannot get or update a user problem that is not one's own. + $t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); + $t->post_ok('/webwork3/api/login' => json => { username => 'moe', password => 'moe' })->status_is(200); -# Check that moe is in the course -my $moe = (grep { $_->{username} eq 'moe' } @$all_users)[0]; + # Check that moe is in the course + my $moe = (grep { $_->{username} eq 'moe' } @$all_users)[0]; -$t->get_ok("/webwork3/api/users/$moe->{user_id}/courses")->status_is(200); -my $moes_courses = $t->tx->res->json; + $t->get_ok("/webwork3/api/users/$moe->{user_id}/courses")->status_is(200); + my $moes_courses = $t->tx->res->json; -ok((grep { $_->{course_name} eq 'Arithmetic' } @$moes_courses)[0], 'Check that moe is in the Arithmetic course.'); + ok((grep { $_->{course_name} eq 'Arithmetic' } @$moes_courses)[0], + 'Check that moe is in the Arithmetic course.'); -# Try to get ralph's user problems -$t->get_ok("/webwork3/api/courses/4/users/$ralph->{user_id}/problems")->status_is(403); + # Try to get ralph's user problems + $t->get_ok("/webwork3/api/courses/4/users/$ralph->{user_id}/problems")->status_is(403); -# Try to get a single problem -$t->get_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}") - ->status_is(403); + # Try to get a single problem + $t->get_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/users/" + . "$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}")->status_is(403); -# Try to update a single problem -$t->put_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}" - => json => { status => 0.5 })->status_is(403); + # Try to update a single problem + $t->put_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/users/" + . "$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}" => json => { status => 0.5 }) + ->status_is(403); -# Switch back to the instructor and delete the user problem -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200); + # Switch back to the instructor and delete the user problem + $t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200); -$t->delete_ok( - "/webwork3/api/courses/4/sets/$hw1->{set_id}/users/$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}") - ->status_is(200)->content_type_is('application/json;charset=UTF-8'); + $t->delete_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/users/" + . "$ralph->{user_id}/problems/$new_user_problem->{user_problem_id}")->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); -# Delete the added problem + # Delete the added problem -$t->delete_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}")->status_is(200) - ->content_type_is('application/json;charset=UTF-8'); + $t->delete_ok("/webwork3/api/courses/4/sets/$hw1->{set_id}/problems/$new_problem->{set_problem_id}") + ->status_is(200)->content_type_is('application/json;charset=UTF-8'); +}; done_testing; diff --git a/t/mojolicious/010_problem_pools.t b/t/mojolicious/010_problem_pools.t index 56a8f9a0..4fd9fcc2 100644 --- a/t/mojolicious/010_problem_pools.t +++ b/t/mojolicious/010_problem_pools.t @@ -1,204 +1,163 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - use Test2::V0; +use Mojo::Base -signatures; use Test2::MojoX; - -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} - -use lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - -use DB::Schema; +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json true false/; use Clone qw/clone/; -use Mojo::JSON qw/true false/; -use YAML::XS qw/LoadFile/; - -use TestUtils qw/loadCSV removeIDs/; - -# Test the api with common "users" routes. - -# Load the config file. -my $config_file = "$main::ww3_dir/conf/webwork3-test.yml"; -$config_file = "$main::ww3_dir/conf/webwork3-test.dist.yml" unless (-e $config_file); -my $config = clone(LoadFile($config_file)); - -# Connect to the database. -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -my $t = Test2::MojoX->new(WeBWorK3 => $config); - -# Login as an user with instructor privileges in a course (Arithmetic; course_id: 4) -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) - ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); - -# Load the review sets from the CSV file -my @pool_problems_from_file = loadCSV( - "$main::ww3_dir/t/db/sample_data/pool_problems.csv", - { - param_non_neg_int_fields => ['library_id'] - } -); - -my @arith_pool_problems = grep { $_->{course_name} eq 'Arithmetic' } @{ clone(\@pool_problems_from_file) }; -for my $problem (@arith_pool_problems) { - # delete $problem->{params}; - delete $problem->{course_name}; -} - -# Get an array of unique problem pools by pool_name. -my %seen; -my @arith_problem_pools = grep { !$seen{ $_->{pool_name} }++ } @{ clone(\@arith_pool_problems) }; -@arith_problem_pools = sort { $a->{pool_name} cmp $b->{pool_name} } @arith_problem_pools; -for my $pool (@arith_problem_pools) { - delete $pool->{params} if defined($pool->{params}); -} - -# Get all problem pools in Arithemtic -$t->get_ok('/webwork3/api/courses/4/pools')->content_type_is('application/json;charset=UTF-8')->status_is(200); - -my @arith_pools_from_db = sort { $a->{pool_name} cmp $b->{pool_name} } @{ $t->tx->res->json }; - -# We want to keep the ids from the pools, so first make a clone to compare -my $arith_pools = clone(\@arith_pools_from_db); -for my $pool (@$arith_pools) { - removeIDs($pool); -} -# my @sorted_arith_pools = sort { $a->{pool_name} cmp $b->{pool_name} } @$arith_pools; -is($arith_pools, \@arith_problem_pools, 'getProblemPools: get problem pools from one course'); - -$t->get_ok("/webwork3/api/courses/4/pools/$arith_pools_from_db[0]->{problem_pool_id}")->status_is(200) - ->json_is('/pool_name' => $arith_problem_pools[0]->{pool_name}); - -# Add a new Problem Pool - -$t->post_ok( - '/webwork3/api/courses/4/pools' => json => { - pool_name => 'integer powers' - } -)->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/pool_name' => 'integer powers'); - -my $added_problem_pool = $t->tx->res->json; - -# Update the problem Pool - -$t->put_ok( - "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}" => json => { - pool_name => 'adding decimals' +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use DBSubtest qw/mojoDBSubtest/; +use BuildDB qw/loadPermissions addCourses addUsers addProblemPools/; +use TestUtils qw/removeIDs/; + +mojoDBSubtest 'user problem routes for review sets' => sub ($t, $schema) { + # Add the neccessary sample data for the test. + loadPermissions($schema, $t->app->home); + addCourses($schema, $t->app->home); + addUsers($schema, $t->app->home); + addProblemPools($schema, $t->app->home); + + # Login as an user with instructor privileges in a course (Arithmetic; course_id: 4) + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/logged_in' => true) + ->json_is('/user/username' => 'lisa')->json_is('/user/is_admin' => false); + + # Load arithmetic problem pools and pool problems from the JSON file. + my (@arith_problem_pools, @arith_pool_problems); + for my $course_info (@{ decode_json($t->app->home->child('t/db/sample_data/pool_problems.json')->slurp) }) { + next unless $course_info->{course_name} eq 'Arithmetic'; + for my $problem_pool_info (@{ $course_info->{pools} }) { + push(@arith_problem_pools, { pool_name => $problem_pool_info->{pool_name} }); + for my $pool_problem (@{ $problem_pool_info->{pool_problems} }) { + push(@arith_pool_problems, { %$pool_problem, pool_name => $problem_pool_info->{pool_name} }); + } + } } -)->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/pool_name' => 'adding decimals'); -# update the local hash ref -$added_problem_pool->{pool_name} = 'adding decimals'; - -# Test Pool Problems -my $prob_pool1 = $arith_pools_from_db[0]; + # Get all problem pools in Arithemtic + $t->get_ok('/webwork3/api/courses/4/pools')->content_type_is('application/json;charset=UTF-8')->status_is(200); -my @arith_pool1 = grep { $_->{pool_name} eq $prob_pool1->{pool_name} } @arith_pool_problems; + my @arith_pools_from_db = sort { $a->{pool_name} cmp $b->{pool_name} } @{ $t->tx->res->json }; -# Get all pool problems from a particular pool in a course - -$t->get_ok("/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problems")->status_is(200) - ->content_type_is('application/json;charset=UTF-8') - ->json_is('/0/params/library_id' => $arith_pool_problems[0]->{params}->{library_id}); - -my $pool_prob1 = $t->tx->res->json->[0]; - -# get a random pool problem - -$t->get_ok("/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problem")->status_is(200) - ->content_type_is('application/json;charset=UTF-8'); - -# get a single pool problem - -$t->get_ok("/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problems/$pool_prob1->{pool_problem_id}") - ->status_is(200)->content_type_is('application/json;charset=UTF-8') - ->json_is('/params/library_id' => $pool_prob1->{params}->{library_id}); - -# add a new pool problem - -$t->post_ok( - "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/problems" => json => { - pool_name => $added_problem_pool->{pool_name}, - params => { library_id => 3492 } + # We want to keep the ids from the pools, so first make a clone to compare + my $arith_pools = clone(\@arith_pools_from_db); + for my $pool (@$arith_pools) { + removeIDs($pool); } -)->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/params/library_id' => 3492); -my $new_pool_problem = $t->tx->res->json; + is($arith_pools, \@arith_problem_pools, 'getProblemPools: get problem pools from one course'); -# update the pool problem + $t->get_ok("/webwork3/api/courses/4/pools/$arith_pools_from_db[0]->{problem_pool_id}")->status_is(200) + ->json_is('/pool_name' => $arith_problem_pools[0]->{pool_name}); -$t->put_ok( - "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/problems/$new_pool_problem->{pool_problem_id}" - => json => { - params => { library_id => 8932 } + # Add a new Problem Pool + $t->post_ok( + '/webwork3/api/courses/4/pools' => json => { + pool_name => 'integer powers' } -)->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/params/library_id' => 8932); + )->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/pool_name' => 'integer powers'); -# Make sure that students don't have access to Problem Pools -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); -$t->post_ok('/webwork3/api/login' => json => { username => 'ralph', password => 'ralph' })->status_is(200); + my $added_problem_pool = $t->tx->res->json; -$t->get_ok('/webwork3/api/courses/4/pools')->status_is(403); -$t->get_ok("/webwork3/api/courses/4/pools/$arith_pools_from_db[0]->{problem_pool_id}")->status_is(403); -$t->post_ok( - '/webwork3/api/courses/4/pools' => json => { - pool_name => 'pool name here' - } -)->status_is(403); -$t->put_ok( - "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}" => json => { - pool_name => 'another name' - } -)->status_is(403); -$t->delete_ok("/webwork3/api/courses/4/pools/$arith_pools_from_db[0]->{problem_pool_id}")->status_is(403); - -# Make sure that students don't have access to Pool Problems -$t->get_ok("/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problems")->status_is(403); -$t->get_ok("/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problem")->status_is(403); -$t->get_ok("/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problems/$pool_prob1->{pool_problem_id}") - ->status_is(403); -$t->post_ok( - "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/problems" => json => { - pool_name => $added_problem_pool->{pool_name}, - params => { library_id => 6188 } - } -)->status_is(403); -$t->put_ok( - "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/problems/$new_pool_problem->{pool_problem_id}" - => json => { - params => { library_id => 8932 } + # Update the problem Pool + $t->put_ok( + "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}" => json => { + pool_name => 'adding decimals' } -)->status_is(403); -$t->delete_ok( - "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/problems/$new_pool_problem->{pool_problem_id}" -)->status_is(403); - -# Cleanup. Log back in as the instructor and delete added pool and problem -$t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); -$t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200); - -# Delete the pool problem. - -$t->delete_ok( - "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/problems/$new_pool_problem->{pool_problem_id}" -)->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/params/library_id' => 8932); - -# Delete the problem pool - -$t->delete_ok("/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}")->status_is(200) - ->content_type_is('application/json;charset=UTF-8')->json_is('/pool_name' => 'adding decimals'); + )->status_is(200)->content_type_is('application/json;charset=UTF-8') + ->json_is('/pool_name' => 'adding decimals'); + # update the local hash ref + $added_problem_pool->{pool_name} = 'adding decimals'; + + # Test Pool Problems + my $prob_pool1 = $arith_pools_from_db[0]; + + my @arith_pool1 = grep { $_->{pool_name} eq $prob_pool1->{pool_name} } @arith_pool_problems; + + # Get all pool problems from a particular pool in a course + $t->get_ok("/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problems")->status_is(200) + ->content_type_is('application/json;charset=UTF-8') + ->json_is('/0/params/library_id' => $arith_pool_problems[0]->{params}->{library_id}); + + my $pool_prob1 = $t->tx->res->json->[0]; + + # get a random pool problem + $t->get_ok("/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problem")->status_is(200) + ->content_type_is('application/json;charset=UTF-8'); + + # get a single pool problem + $t->get_ok( + "/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problems/$pool_prob1->{pool_problem_id}") + ->status_is(200)->content_type_is('application/json;charset=UTF-8') + ->json_is('/params/library_id' => $pool_prob1->{params}->{library_id}); + + # add a new pool problem + $t->post_ok( + "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/problems" => json => { + pool_name => $added_problem_pool->{pool_name}, + params => { library_id => 3492 } + } + )->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/params/library_id' => 3492); + + my $new_pool_problem = $t->tx->res->json; + + # update the pool problem + $t->put_ok( + "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/" + . "problems/$new_pool_problem->{pool_problem_id}" => json => { + params => { library_id => 8932 } + } + )->status_is(200)->content_type_is('application/json;charset=UTF-8')->json_is('/params/library_id' => 8932); + + # Make sure that students don't have access to Problem Pools + $t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); + $t->post_ok('/webwork3/api/login' => json => { username => 'ralph', password => 'ralph' })->status_is(200); + + $t->get_ok('/webwork3/api/courses/4/pools')->status_is(403); + $t->get_ok("/webwork3/api/courses/4/pools/$arith_pools_from_db[0]->{problem_pool_id}")->status_is(403); + $t->post_ok( + '/webwork3/api/courses/4/pools' => json => { + pool_name => 'pool name here' + } + )->status_is(403); + $t->put_ok("/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}" => json => + { pool_name => 'another name' })->status_is(403); + $t->delete_ok("/webwork3/api/courses/4/pools/$arith_pools_from_db[0]->{problem_pool_id}")->status_is(403); + + # Make sure that students don't have access to Pool Problems + $t->get_ok("/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problems")->status_is(403); + $t->get_ok("/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problem")->status_is(403); + $t->get_ok( + "/webwork3/api/courses/4/pools/$prob_pool1->{problem_pool_id}/problems/$pool_prob1->{pool_problem_id}") + ->status_is(403); + $t->post_ok( + "/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/problems" => json => { + pool_name => $added_problem_pool->{pool_name}, + params => { library_id => 6188 } + } + )->status_is(403); + $t->put_ok("/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/" + . "problems/$new_pool_problem->{pool_problem_id}" => json => { params => { library_id => 8932 } }) + ->status_is(403); + $t->delete_ok("/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/" + . "problems/$new_pool_problem->{pool_problem_id}")->status_is(403); + + # Cleanup. Log back in as the instructor and delete added pool and problem + $t->post_ok('/webwork3/api/logout')->status_is(200)->json_is('/logged_in' => false); + $t->post_ok('/webwork3/api/login' => json => { username => 'lisa', password => 'lisa' })->status_is(200); + + # Delete the pool problem. + $t->delete_ok("/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}/" + . "problems/$new_pool_problem->{pool_problem_id}")->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/params/library_id' => 8932); + + # Delete the problem pool + $t->delete_ok("/webwork3/api/courses/4/pools/$added_problem_pool->{problem_pool_id}")->status_is(200) + ->content_type_is('application/json;charset=UTF-8')->json_is('/pool_name' => 'adding decimals'); +}; done_testing(); diff --git a/tests/stores/courses.spec.ts b/tests/stores/courses.spec.ts index 13b38dd6..82d9c768 100644 --- a/tests/stores/courses.spec.ts +++ b/tests/stores/courses.spec.ts @@ -1,11 +1,8 @@ -/** - * @jest-environment jsdom - */ -// The above is needed because 1) the logger uses the window object, which is only present +/** @jest-environment jsdom */ +// The above is needed because 1) the logger uses the window object, which is only present // when using the jsdom environment and 2) because the pinia store is used is being // tested with persistance. -// courses.spec.ts // Test the Course Store import { createApp } from 'vue'; @@ -15,14 +12,14 @@ import { api } from 'boot/axios'; import { useCourseStore } from 'src/stores/courses'; import { Course } from 'src/common/models/courses'; -import { cleanIDs, loadCSV } from '../utils'; +import { cleanIDs } from '../utils'; describe('Test the course store', () => { const app = createApp({}); describe('Set up the Course Store', () => { - let courses_from_csv: Course[]; + const courses_from_json: Course[] = []; beforeAll(async () => { // Since we have the piniaPluginPersistedState as a plugin, duplicate for the test. @@ -30,15 +27,9 @@ describe('Test the course store', () => { app.use(pinia); setActivePinia(pinia); - const parsed_courses = await loadCSV('t/db/sample_data/courses.csv', { - boolean_fields: ['visible'], - non_neg_int_fields: ['course_id'], - params: ['course_dates', 'course_params'] - }); - courses_from_csv = parsed_courses.map(course => { - delete course.course_params; - return new Course(course); - }); + (await import('../../t/db/sample_data/courses.json')).default.map( + (course) => courses_from_json.push(new Course(course)) + ); // Login to the course as the admin in order to be authenticated for the rest of the test. await api.post('login', { username: 'admin', password: 'admin' }); @@ -47,7 +38,7 @@ describe('Test the course store', () => { test('Fetch the courses', async () => { const course_store = useCourseStore(); await course_store.fetchCourses(); - expect(cleanIDs(course_store.courses)).toStrictEqual(cleanIDs(courses_from_csv)); + expect(cleanIDs(course_store.courses)).toStrictEqual(cleanIDs(courses_from_json)); }); }); diff --git a/tests/stores/permissions.spec.ts b/tests/stores/permissions.spec.ts index 8bdd7c7c..34aaf778 100644 --- a/tests/stores/permissions.spec.ts +++ b/tests/stores/permissions.spec.ts @@ -1,11 +1,8 @@ -/** - * @jest-environment jsdom - */ +/** @jest-environment jsdom */ // The above is needed because 1) the logger uses the window object, which is only present // when using the jsdom environment and 2) because the pinia store is used is being // tested with persistance. -// courses.spec.ts // Test the Course Store import { createApp } from 'vue'; diff --git a/tests/stores/problem_sets.spec.ts b/tests/stores/problem_sets.spec.ts index 41d1aa27..303db243 100644 --- a/tests/stores/problem_sets.spec.ts +++ b/tests/stores/problem_sets.spec.ts @@ -1,11 +1,8 @@ -/** - * @jest-environment jsdom - */ +/** @jest-environment jsdom */ // The above is needed because 1) the logger uses the window object, which is only present // when using the jsdom environment and 2) because the pinia store is used is being // tested with persistance. -// problem_sets.spec.ts // Test the problem sets Store import { setActivePinia, createPinia } from 'pinia'; @@ -19,14 +16,15 @@ import { useSessionStore } from 'src/stores/session'; import { HomeworkSet, ProblemSet, Quiz, ReviewSet } from 'src/common/models/problem_sets'; import { Course } from 'src/common/models/courses'; -import { cleanIDs, loadCSV } from '../utils'; +import { cleanIDs } from '../utils'; import { checkPassword } from 'src/common/api-requests/session'; const app = createApp({}); describe('Problem Set store tests', () => { - let problem_sets_from_csv: ProblemSet[]; + const problem_sets_from_json: ProblemSet[] = []; let precalc_course: Course; + beforeAll(async () => { // Since we have the piniaPluginPersistedState as a plugin, duplicate for the test. const pinia = createPinia().use(piniaPluginPersistedstate); @@ -39,27 +37,17 @@ describe('Problem Set store tests', () => { session_store.updateSessionInfo(session_info); await session_store.fetchUserCourses(); - const problem_set_config = { - params: ['set_params', 'set_dates' ], - boolean_fields: ['set_visible'], - param_boolean_fields: ['timed', 'enable_reduced_scoring', 'can_retake'], - param_non_neg_int_fields: ['quiz_duration'] - }; - - const hw_sets_to_parse = await loadCSV('t/db/sample_data/hw_sets.csv', problem_set_config); - const hw_sets_from_csv = hw_sets_to_parse.filter(set => set.course_name === 'Precalculus') - .map(set => new HomeworkSet(set)); - - const quizzes_to_parse = await loadCSV('t/db/sample_data/quizzes.csv', problem_set_config); - const quizzes_from_csv = quizzes_to_parse.filter(set => set.course_name === 'Precalculus') - .map(q => new Quiz(q)); + (await import('../../t/db/sample_data/hw_sets.json')).default + .find((course) => course.course_name === 'Precalculus') + ?.sets.forEach((set) => problem_sets_from_json.push(new HomeworkSet(set))); - const review_sets_to_parse = await loadCSV('t/db/sample_data/review_sets.csv', problem_set_config); - const review_sets_from_csv = review_sets_to_parse.filter(set => set.course_name === 'Precalculus') - .map(set => new ReviewSet(set)); + (await import('../../t/db/sample_data/quizzes.json')).default + .find((course) => course.course_name === 'Precalculus') + ?.sets.forEach((set) => problem_sets_from_json.push(new Quiz(set))); - // combine quizzes, review sets and homework sets - problem_sets_from_csv = [...hw_sets_from_csv, ...quizzes_from_csv, ...review_sets_from_csv]; + (await import('../../t/db/sample_data/review_sets.json')).default + .find((course) => course.course_name === 'Precalculus') + ?.sets.forEach((set) => problem_sets_from_json.push(new ReviewSet(set))); // We'll need the courses as well. const courses_store = useCourseStore(); @@ -83,7 +71,7 @@ describe('Problem Set store tests', () => { expect(problem_set_store.problem_sets.length).toBeGreaterThan(0); expect(sortAndClean(problem_set_store.problem_sets as ProblemSet[])) - .toStrictEqual(sortAndClean(problem_sets_from_csv)); + .toStrictEqual(sortAndClean(problem_sets_from_json)); }); }); diff --git a/tests/stores/session.spec.ts b/tests/stores/session.spec.ts index f26b224f..4953ccd3 100644 --- a/tests/stores/session.spec.ts +++ b/tests/stores/session.spec.ts @@ -1,11 +1,8 @@ -/** - * @jest-environment jsdom - */ +/** @jest-environment jsdom */ // The above is needed because 1) the logger uses the window object, which is only present // when using the jsdom environment and 2) because the pinia store is used is being // tested with persistance. -// session.spec.ts // Test the Session Store import { createApp } from 'vue'; @@ -21,12 +18,12 @@ import { Course, UserCourse } from 'src/common/models/courses'; import { SessionInfo } from 'src/common/models/session'; import { ParseableUser, User } from 'src/common/models/users'; -import { cleanIDs, loadCSV } from '../utils'; +import { cleanIDs } from '../utils'; const app = createApp({}); describe('Session Store', () => { - let lisa_courses: UserCourse[]; + const lisa_courses: UserCourse[] = []; let lisa: User; // session now just stores objects not models: @@ -65,39 +62,30 @@ describe('Session Store', () => { await api.post('login', { username: 'admin', password: 'admin' }); // Load the user course information for testing later. - const parsed_courses = await loadCSV('t/db/sample_data/courses.csv', { - boolean_fields: ['visible'], - non_neg_int_fields: ['course_id'], - params: ['course_dates', 'course_params'] - }); + const courses_from_json = (await import('../../t/db/sample_data/courses.json')).default.map( + (course) => new Course(course) + ); - const courses_from_csv = parsed_courses.map(course => { - delete course.course_params; - return new Course(course); - }); - - const users_to_parse = await loadCSV('t/db/sample_data/students.csv', { - boolean_fields: ['is_admin'], - non_neg_int_fields: ['user_id'] - }); + const users_to_parse = (await import('../../t/db/sample_data/users.json')).default; // Fetch the user lisa. This is used below. lisa = new User(await getUser('lisa')); - lisa_courses = users_to_parse.filter(user => user.username === 'lisa') - .map(user_course => { - const course = courses_from_csv.find(c => c.course_name == user_course.course_name) - ?? new Course(); - return new UserCourse({ - course_id: 0, // will this be stripped before comparison later? - course_name: course.course_name, - user_id: lisa.user_id, - visible: course.visible, - role: user_course.role as string, - course_dates: course.course_dates - }); + users_to_parse + .find((user) => user.username === 'lisa') + ?.courses?.forEach((course_info) => { + const course = courses_from_json.find((c) => c.course_name == course_info.course_name) ?? new Course(); + lisa_courses.push( + new UserCourse({ + course_id: 0, + course_name: course.course_name, + user_id: lisa.user_id, + visible: course.visible, + role: course_info.course_user.role, + course_dates: course.course_dates, + }) + ); }); - }); describe('Testing the Session Store.', () => { diff --git a/tests/stores/set_problems.spec.ts b/tests/stores/set_problems.spec.ts index 0d4bfb2d..eee4e487 100644 --- a/tests/stores/set_problems.spec.ts +++ b/tests/stores/set_problems.spec.ts @@ -1,11 +1,8 @@ -/** - * @jest-environment jsdom - */ +/** @jest-environment jsdom */ // The above is needed because 1) the logger uses the window object, which is only present // when using the jsdom environment and 2) because the pinia store is used is being // tested with persistance. -// set_problems.spec.ts // Test the set problems store import { createApp } from 'vue'; @@ -25,18 +22,17 @@ import { UserProblem, ParseableSetProblem, parseProblem, SetProblem, SetProblemP import { DBUserHomeworkSet, mergeUserSet, UserSet } from 'src/common/models/user_sets'; import { Dictionary, generic } from 'src/common/models'; -import { loadCSV, cleanIDs } from '../utils'; +import { cleanIDs } from '../utils'; import { checkPassword } from 'src/common/api-requests/session'; -import { logger } from 'src/boot/logger'; const app = createApp({}); describe('Problem Set store tests', () => { let precalc_course: Course; // These are needed for multiple tests. - let precalc_merged_problems: UserProblem[]; + const precalc_merged_problems: UserProblem[] = []; let precalc_hw1_user_problems: UserProblem[]; - let precalc_problems_from_csv: Dictionary>[]; + const precalc_problems_from_json: Dictionary>[] = []; beforeAll(async () => { // Since we have the piniaPluginPersistedState as a plugin, duplicate for the test. @@ -49,62 +45,50 @@ describe('Problem Set store tests', () => { const session_store = useSessionStore(); session_store.updateSessionInfo(session_info); - const problem_set_config = { - params: ['set_params', 'set_dates' ], - boolean_fields: ['set_visible'], - param_boolean_fields: ['timed', 'enable_reduced_scoring', 'can_retake'], - param_non_neg_int_fields: ['quiz_duration'] - }; - - const hw_sets_to_parse = await loadCSV('t/db/sample_data/hw_sets.csv', problem_set_config); - const hw_sets_from_csv = hw_sets_to_parse.filter(set => set.course_name === 'Precalculus') - .map(set => new HomeworkSet(set)); - - const quizzes_to_parse = await loadCSV('t/db/sample_data/quizzes.csv', problem_set_config); - const quizzes_from_csv = quizzes_to_parse.filter(set => set.course_name === 'Precalculus') - .map(q => new Quiz(q)); - - const review_sets_to_parse = await loadCSV('t/db/sample_data/review_sets.csv', problem_set_config); - const review_sets_from_csv = review_sets_to_parse.filter(set => set.course_name === 'Precalculus') - .map(set => new ReviewSet(set)); - - // combine quizzes, review sets and homework sets - const problem_sets_from_csv = [...hw_sets_from_csv, ...quizzes_from_csv, ...review_sets_from_csv]; - - // Load all problems from CSV files - const problems_to_parse = await loadCSV('t/db/sample_data/problems.csv', { - params: ['problem_params'], - non_neg_int_fields: ['problem_number'], - param_non_neg_float_fields: ['weight'], - param_non_neg_int_fields: ['library_id', 'problem_pool_id'] - }); - - // Filter only Precalc problems and remove any undefined library_ids - precalc_problems_from_csv = problems_to_parse - .filter(prob => prob.course_name === 'Precalculus'); - - // Load the User Problem information from the csv file: - const user_problems_from_csv = await loadCSV('t/db/sample_data/user_problems.csv', { - params: [], - non_neg_int_fields: ['problem_number', 'seed'], - non_neg_float_fields: ['status'] - }); - - // load all user problems in Precalc from CSV files and merge them with set problems. - // Load the users from the csv file. - const users_to_parse = await loadCSV('t/db/sample_data/students.csv', { - boolean_fields: ['is_admin'], - non_neg_int_fields: ['user_id'] - }); + const problem_sets_from_json: ProblemSet[] = []; + + (await import('../../t/db/sample_data/hw_sets.json')).default + .find((course) => course.course_name === 'Precalculus') + ?.sets.forEach((set) => problem_sets_from_json.push(new HomeworkSet(set))); + + (await import('../../t/db/sample_data/quizzes.json')).default + .find((course) => course.course_name === 'Precalculus') + ?.sets.forEach((set) => problem_sets_from_json.push(new Quiz(set))); + + (await import('../../t/db/sample_data/review_sets.json')).default + .find((course) => course.course_name === 'Precalculus') + ?.sets.forEach((set) => problem_sets_from_json.push(new ReviewSet(set))); + + // Load problems from the JSON file and extract the precalc problems. + (await import('../../t/db/sample_data/problems.json')).default + .find((course) => course.course_name === 'Precalculus')?.sets.forEach((set) => { + set.problems.forEach((problem) => { + precalc_problems_from_json.push({ + problem_number: problem.problem_number, + problem_params: problem.problem_params, + set_name: set.set_name + }); + }); + }); - precalc_merged_problems = user_problems_from_csv - .filter(user_problem => user_problem.course_name === 'Precalculus') - .map(user_problem => { - const problem_set = problem_sets_from_csv.find(set => set.set_name === user_problem.set_name); - const set_problem = precalc_problems_from_csv.find(prob => - prob.set_name === problem_set?.set_name && prob.problem_number === user_problem.problem_number); - const user = users_to_parse.find(user => user.username === user_problem.username); - return new UserProblem(Object.assign({}, set_problem, user_problem, user)); + // Load the users from the JSON file. + const users_to_parse = (await import('../../t/db/sample_data/users.json')).default; + + // Load the user problem information from the JSON file and merge them with the set problems. + (await import('../../t/db/sample_data/user_problems.json')).default + .find((course) => course.course_name === 'Precalculus')?.sets.forEach((set_info) => { + const problem_set = problem_sets_from_json.find((set) => set.set_name === set_info.set_name); + set_info.problems.forEach((problem_info) => { + const set_problem = precalc_problems_from_json.find((prob) => + prob.set_name === problem_set?.set_name && prob.problem_number === problem_info.problem_number); + problem_info.users.forEach((user_info) => { + const user = users_to_parse.find(user => user.username === user_info.username); + precalc_merged_problems.push( + new UserProblem(Object.assign({}, set_problem, user_info.user_problem, user)) + ); + }); + + }); }); precalc_hw1_user_problems = precalc_merged_problems.filter(prob => prob.set_name === 'HW #1'); @@ -131,10 +115,10 @@ describe('Problem Set store tests', () => { expect(set_problem_store.set_problems.length).toBeGreaterThan(0); // Create Problems as Models and then convert to Object to compare. - const precalc_problems = precalc_problems_from_csv + const precalc_problems = precalc_problems_from_json .map(prob => parseProblem(prob as ParseableSetProblem, 'Set') as SetProblem); - expect(cleanIDs(set_problem_store.set_problems)) - .toStrictEqual(cleanIDs(precalc_problems)); + + expect(cleanIDs(set_problem_store.set_problems)).toStrictEqual(cleanIDs(precalc_problems)); }); }); diff --git a/tests/stores/user_sets.spec.ts b/tests/stores/user_sets.spec.ts index d5c40249..3135059e 100644 --- a/tests/stores/user_sets.spec.ts +++ b/tests/stores/user_sets.spec.ts @@ -1,17 +1,13 @@ -/** - * @jest-environment jsdom - */ +/** @jest-environment jsdom */ // The above is needed because 1) the logger uses the window object, which is only present // when using the jsdom environment and 2) because the pinia store is used is being // tested with persistance. -// problem_sets.spec.ts // Test the user sets Store import { createApp } from 'vue'; import { createPinia, setActivePinia } from 'pinia'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; -import { api } from 'boot/axios'; import { useCourseStore } from 'src/stores/courses'; import { useProblemSetStore } from 'src/stores/problem_sets'; @@ -19,22 +15,20 @@ import { useSessionStore } from 'src/stores/session'; import { useUserStore } from 'src/stores/users'; import { Course } from 'src/common/models/courses'; -import { - ProblemSet, HomeworkSet, Quiz, ReviewSet, ParseableProblemSetDates, ParseableProblemSetParams -} from 'src/common/models/problem_sets'; +import { ProblemSet, HomeworkSet, Quiz, ReviewSet } from 'src/common/models/problem_sets'; import { CourseUser } from 'src/common/models/users'; import { UserSet, UserHomeworkSet, UserQuiz, UserReviewSet, mergeUserSet, parseDBUserSet, DBUserHomeworkSet } from 'src/common/models/user_sets'; -import { loadCSV, cleanIDs } from '../utils'; +import { cleanIDs } from '../utils'; import { checkPassword } from 'src/common/api-requests/session'; const app = createApp({}); describe('Tests user sets and merged user sets in the problem set store', () => { - let problem_sets_from_csv: ProblemSet[]; - let precalc_user_sets: UserSet[]; + const problem_sets_from_json: ProblemSet[] = []; + const precalc_user_sets: UserSet[] = []; let precalc_course: Course; beforeAll(async () => { @@ -49,27 +43,17 @@ describe('Tests user sets and merged user sets in the problem set store', () => session_store.updateSessionInfo(session_info); await session_store.fetchUserCourses(); - const problem_set_config = { - params: ['set_params', 'set_dates' ], - boolean_fields: ['set_visible'], - param_boolean_fields: ['timed', 'enable_reduced_scoring', 'can_retake'], - param_non_neg_int_fields: ['quiz_duration'] - }; - - const hw_sets_to_parse = await loadCSV('t/db/sample_data/hw_sets.csv', problem_set_config); - const hw_sets_from_csv = hw_sets_to_parse.filter(set => set.course_name === 'Precalculus') - .map(set => new HomeworkSet(set)); + (await import('../../t/db/sample_data/hw_sets.json')).default + .find((course) => course.course_name === 'Precalculus') + ?.sets.map((set) => problem_sets_from_json.push(new HomeworkSet(set))); - const quizzes_to_parse = await loadCSV('t/db/sample_data/quizzes.csv', problem_set_config); - const quizzes_from_csv = quizzes_to_parse.filter(set => set.course_name === 'Precalculus') - .map(q => new Quiz(q)); + (await import('../../t/db/sample_data/quizzes.json')).default + .find((course) => course.course_name === 'Precalculus') + ?.sets.map((set) => problem_sets_from_json.push(new Quiz(set))); - const review_sets_to_parse = await loadCSV('t/db/sample_data/review_sets.csv', problem_set_config); - const review_sets_from_csv = review_sets_to_parse.filter(set => set.course_name === 'Precalculus') - .map(set => new ReviewSet(set)); - - // combine quizzes, review sets and homework sets - problem_sets_from_csv = [...hw_sets_from_csv, ...quizzes_from_csv, ...review_sets_from_csv]; + (await import('../../t/db/sample_data/review_sets.json')).default + .find((course) => course.course_name === 'Precalculus') + ?.sets.map((set) => problem_sets_from_json.push(new ReviewSet(set))); // We'll need the courses as well. const courses_store = useCourseStore(); @@ -93,45 +77,45 @@ describe('Tests user sets and merged user sets in the problem set store', () => await problem_set_store.fetchAllUserSets(precalc_course.course_id); expect(problem_set_store.user_sets.length).toBeGreaterThan(0); - // Setup and load the user sets data from a csv file. - const user_sets_to_parse = await loadCSV('t/db/sample_data/user_sets.csv', { - params: ['set_dates', 'set_params'], - boolean_fields: ['set_visible'], - param_boolean_fields: ['timed', 'enable_reduced_scoring', 'can_retake'], - param_non_neg_int_fields: ['quiz_duration'] - }); + // Load the user sets data from the JSON file and select the precalculus users. + const precalc_user_sets_to_parse = (await import('../../t/db/sample_data/user_sets.json')).default + .find((course) => course.course_name === 'Precalculus')?.sets; - // Load the users from the CSV file and filter only the Precalc students. - const users_to_parse = await loadCSV('t/db/sample_data/students.csv', { - boolean_fields: ['is_admin'], - non_neg_int_fields: ['user_id'] - }); + // Load the users from the JSON file. + const users_to_parse = (await import('../../t/db/sample_data/users.json')).default; - const precalc_merged_users = users_to_parse.filter(user => user.course_name === 'Precalculus') - .map(user => new CourseUser(user)); + const precalc_merged_users: CourseUser[] = []; + for (const user of users_to_parse) { + const precalcCourseUser = user.courses?.find((course) => course.course_name === 'Precalculus'); + if (!precalcCourseUser) continue; + precalc_merged_users.push(new CourseUser({ ...user, ...precalcCourseUser.course_user })); + } - // Filter only user sets from HW #1 - const hw1_from_csv = user_sets_to_parse - .filter(set => set.course_name === 'Precalculus' && set.set_name === 'HW #1') - .map(set => new DBUserHomeworkSet(set)); + // Filter only user sets from "HW #1". + const hw1_from_json: DBUserHomeworkSet[] = []; + precalc_user_sets_to_parse?.find((set) => set.set_name === 'HW #1') + ?.users.map((user) => hw1_from_json.push(new DBUserHomeworkSet(user.user_set))); const hw1 = problem_set_store.findProblemSet({ set_name: 'HW #1' }); const db_user_sets_from_store = problem_set_store.db_user_sets .filter(set => set.set_id === hw1?.set_id); - expect(cleanIDs(db_user_sets_from_store)).toStrictEqual(cleanIDs(hw1_from_csv)); + expect(cleanIDs(db_user_sets_from_store)).toStrictEqual(cleanIDs(hw1_from_json)); - precalc_user_sets = user_sets_to_parse - .filter(set => set.course_name === 'Precalculus') - .map(obj => { - const problem_set = problem_sets_from_csv.find(set => set.set_name == obj.set_name); - const merged_user = precalc_merged_users.find(u => u.username === obj.username) ?? new CourseUser(); + precalc_user_sets_to_parse?.map((set_info) => { + const problem_set = problem_sets_from_json.find(set => set.set_name == set_info.set_name); + set_info.users.map((user) => { + const merged_user = + precalc_merged_users.find(u => u.username === user.username) ?? new CourseUser(); const user_set = parseDBUserSet({ set_type: problem_set?.set_type, - set_dates: obj.set_dates as ParseableProblemSetDates, - set_params: obj.set_params as ParseableProblemSetParams + set_dates: user.user_set.set_dates, + set_params: {} }); - return mergeUserSet(problem_set as ProblemSet, user_set, merged_user) ?? new UserSet(); + precalc_user_sets.push( + mergeUserSet(problem_set as ProblemSet, user_set, merged_user) ?? new UserSet() + ); }); + }); }); }); let new_hw_set: ProblemSet; diff --git a/tests/stores/users.spec.ts b/tests/stores/users.spec.ts index c6907599..1b52f61d 100644 --- a/tests/stores/users.spec.ts +++ b/tests/stores/users.spec.ts @@ -1,11 +1,8 @@ -/** - * @jest-environment jsdom - */ +/** @jest-environment jsdom */ // The above is needed because 1) the logger uses the window object, which is only present // when using the jsdom environment and 2) because the pinia store is used is being // tested with persistance. -// users.spec.ts // Test the user Store import { createApp } from 'vue'; @@ -15,7 +12,7 @@ import { api } from 'boot/axios'; import { useCourseStore } from 'src/stores/courses'; import { useUserStore } from 'src/stores/users'; -import { cleanIDs, loadCSV } from '../utils'; +import { cleanIDs } from '../utils'; import { Course } from 'src/common/models/courses'; import { CourseUser, User } from 'src/common/models/users'; @@ -23,9 +20,9 @@ import { CourseUser, User } from 'src/common/models/users'; const app = createApp({}); describe('User store tests', () => { - let global_users: User[]; - let precalc_global_users: User[]; - let precalc_users: CourseUser[]; + const global_users: User[] = []; + const precalc_global_users: User[] = []; + const precalc_users: CourseUser[] = []; let precalc_course: Course; beforeAll(async () => { // Since we have the piniaPluginPersistedState as a plugin, duplicate for the test. @@ -36,22 +33,19 @@ describe('User store tests', () => { // Login to the course as the admin in order to be authenticated for the rest of the test. await api.post('login', { username: 'admin', password: 'admin' }); - const users_to_parse = await loadCSV('t/db/sample_data/students.csv', { - boolean_fields: ['is_admin'], - non_neg_int_fields: ['user_id'] - }); + // Load the users from the JSON file. + const users = (await import('../../t/db/sample_data/users.json')).default; + + for (const user of users) { + if (user.username === 'admin') continue; + global_users.push(new User(user)); + + const precalcCourseUser = user.courses?.find((course) => course.course_name === 'Precalculus'); + if (!precalcCourseUser) continue; - // Do some parsing and cleanup. - const all_users_from_csv = users_to_parse.map(user => new User(user)); - precalc_global_users = users_to_parse.filter(user => user.course_name === 'Precalculus') - .map(user => new User(user)); - precalc_users = users_to_parse.filter(user => user.course_name === 'Precalculus') - .map(user => new CourseUser(user)); - - // remove duplicates (for users in multiple courses) for global users - const usernames = [... new Set(all_users_from_csv.map(user => user.username))]; - global_users = usernames.map(username => all_users_from_csv - .find(user => user.username === username) ?? new User()); + precalc_global_users.push(new User(user)); + precalc_users.push(new CourseUser({ ...user, ...precalcCourseUser.course_user })); + } // fetch the courses, so we can get the course_id of desired course. const course_store = useCourseStore(); diff --git a/tests/utils.ts b/tests/utils.ts index 867f0f1b..6b53c630 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,117 +1,11 @@ -// utils.ts // Utility functions for testing -import papa from 'papaparse'; -import fs from 'fs'; import { Dictionary, generic, Model } from 'src/common/models'; -import { parseBoolean, parseNonNegDecimal, parseNonNegInt } from 'src/common/models/parsers'; /** - * Used for parsing a csv file. The params field is an array of strings that are in stored as - * a JSON file in the database (typically a params field or dates field.). The boolean_fields is - * an array of strings that are boolean fields and the non_neg_fields is an array of strings with - * fields that are non-nonegative integers (often database ids). - */ - -interface CSVConfig { - params?: string[]; - boolean_fields?: string[]; - non_neg_int_fields?: string[]; - non_neg_float_fields?: string[]; - param_boolean_fields?: string[]; - param_non_neg_int_fields?: string[]; - param_non_neg_float_fields?: string[]; -} - -/** - * Convert the data in the form of an array of objects of strings or numbers and converts to the proper - * form to pass to a desired model. These typically come from a CSV file where the dates and params - * are located in separate columns in the CSV file and are converted to a nested object. In addition, - * booleans and integers are parsed. - */ - -function convert(data: Dictionary[], config: CSVConfig): Dictionary>[] { - const keys = Object.keys(data[0]); - const param_fields = config.params ?? []; - const param_boolean_fields = config.param_boolean_fields ?? []; - const param_non_neg_int_fields = config.param_non_neg_int_fields ?? []; - const param_non_neg_float_fields = config.param_non_neg_float_fields ?? []; - - // Store the param fields and the matching regular expressions - const p_fields: Dictionary = {}; - param_fields.forEach(key => { - const regexp = RegExp('^' + key.toUpperCase() + ':([\\w_]+)'); - p_fields[key] = keys.filter(k => regexp.test(k)); - }); - - const all_param_fields = Object.entries(p_fields) - .reduce((prev, [, value]) => prev = [...prev, ...value], [] as string[]); - const known_fields = [...all_param_fields, ...(config.boolean_fields ?? []), - ...(config.non_neg_int_fields ?? []), ...(config.non_neg_float_fields ?? [])]; - const other_fields = keys.filter(k => known_fields.indexOf(k) < 0); - - return data.map(row => { - const d: Dictionary> = {}; - // All non-param, non-boolean and non-integer fields don't need to be parsed. - other_fields.forEach(key => { d[key] = row[key]; }); - // Parse boolean fields - (config.boolean_fields ?? []).forEach(key => { - if (row[key] != undefined) d[key] = parseBoolean(row[key]); - }); - // Parse int fields - (config.non_neg_int_fields ?? []).forEach(key => { - if (row[key] != undefined) d[key] = parseNonNegInt(row[key]); - }); - // Parse float fields - (config.non_neg_float_fields ?? []).forEach(key => { - if (row[key] != undefined) d[key] = parseNonNegDecimal(row[key]); - }); - // Parse parameter fields - Object.entries(p_fields).forEach(([key, ]) => { - d[key] = p_fields[key].reduce((prev: Dictionary, val) => { - const field = val.split(':')[1]; - // Parse any date field as date. - - if (row[val]) { - // parse booleans, floats and integers - prev[field] = - param_boolean_fields.includes(field) ? (parseInt(row[val]) === 1 ? true : false) : - param_non_neg_int_fields.includes(field) ? parseInt(row[val]) : - param_non_neg_float_fields.includes(field) ? parseFloat(row[val]) : - /DATES:/.test(val) ? - Date.parse(row[val]) / 1000 : - row[val]; - } - return prev; - }, {}); - }); - return d; - }); -}; - -/** - * Load and parse a CSV file with given filepath and config file. - */ - -export async function loadCSV(filepath: string, config: CSVConfig): - Promise< Dictionary>[]> { - const file = fs.createReadStream(filepath); - return new Promise((resolve, reject) => { - papa.parse(file, { - header: true, - complete (results) { - return resolve(convert(results.data as Dictionary[], config)); - }, - error (err) { - return reject(err); - } - }); - }); -} - -/** - * Removes all fields ending in _id. This is useful for comparing data from the database where the - * internal _id are not important. This returns an array of objects without the _id fields. + * Removes all fields ending in _id. This is useful for comparing data from + * the database where the internal _id are not important. This returns an array + * of objects without the _id fields. */ export const cleanIDs = (m: Model | Model[]): Dictionary | Dictionary[] => {