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 0b2e6f24..22864619 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,62 +1,40 @@ --- -########################### -## 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.8.1 + uses: github/super-linter@v4.9.5 env: VALIDATE_ALL_CODEBASE: false @@ -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 8f97781f..a8049e25 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -1,30 +1,85 @@ +--- name: Unit Tests and Coverage on: push: - 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: Run perl unit tests + - 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: - HARNESS_PERL_SWITCHES: -MDevel::Cover + DEBIAN_FRONTEND: noninteractive + DEBCONF_NONINTERACTIVE_SEEN: true + DEBCONF_NOWARNINGS: yes run: | - perl t/db/build_db.pl - prove -r t + 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 \ + libdbd-mysql-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 \ + libsub-info-perl \ + libterm-table-perl \ + libtest-harness-perl \ + libtest2-suite-perl \ + libtest-postgresql-perl \ + libtext-csv-perl \ + libtry-tiny-perl \ + libyaml-libyaml-perl \ + mariadb-client \ + mariadb-server \ + postgresql + cpanm --sudo --notest \ + DBIx::Class::DynamicSubclass \ + Mojolicious::Plugin::DBIC \ + Mojolicious::Plugin::NotYAMLConfig \ + Test2::MojoX \ + Devel::Cover::Report::Codecov - # we probably don'te need to upload the codecov data - # - uses: actions/upload-artifact@v2 - # with: - # name: coverage-report - # path: cover_db/ + - name: Run perl unit tests + env: + HARNESS_PERL_SWITCHES: -MDevel::Cover + WW3_TEST_ALL_DBS: 1 + run: prove -r t - name: Push coverage analysis if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} @@ -32,12 +87,19 @@ jobs: 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 + + - 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 5a28ef7f..784e32ac 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_fcn +[Freenode::DollarAB] +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/docker/webwork3.dockerfile b/docker/webwork3.dockerfile index d5ec402f..334ad370 100644 --- a/docker/webwork3.dockerfile +++ b/docker/webwork3.dockerfile @@ -1,46 +1,66 @@ -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 \ + 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 \ + 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 \ + 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 \ + libsub-info-perl=0.015-2 \ + libterm-table-perl=0.015-2 \ 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 && \ + libtest2-suite-perl=0.000144-1 \ + 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 \ + Test2::MojoX \ 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/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 0b2b72d8..9b35bc92 100644 --- a/t/db/001_courses.t +++ b/t/db/001_courses.t @@ -2,162 +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 lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - -use Test::More; -use Test::Exception; -use YAML::XS qw/LoadFile/; -use DateTime::Format::Strptime; - -use DB::Schema; - -use TestUtils qw/loadCSV 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 } -); -my $strp = DateTime::Format::Strptime->new(pattern => '%F', on_error => 'croak'); - -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_deeply(\@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_deeply($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'); - -# 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'; - -throws_ok { - $course_rs->getCourse(info => { name => 'Calculus' }); -} -'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'; - -# Add a course -my $new_course_params = { - course_name => 'Geometry', - visible => 1, - course_dates => {} +use Test2::V0; + +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; + +use Mojo::File qw/curfile/; +use Test::PostgreSQL; +use Mojo::JSON qw/true/; + +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' + ); }; -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'); - -# Add a course that already exists -throws_ok { - $course_rs->addCourse(params => { course_name => 'Geometry', visible => 1 }); -} -'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_deeply($new_course_params, $updated_course, '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'; - -throws_ok { - $course_rs->updateCourse(info => { course_id => -9 }, params => $new_course_params); -} -'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'); - -# 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'; - -# 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'; - -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_deeply(\@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..928c5922 100644 --- a/t/db/002_course_settings.t +++ b/t/db/002_course_settings.t @@ -2,261 +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 Test::More; -use Test::Exception; -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 validateSingleCourseSetting 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. + is( + validateSettingConfig({ + var => 'my_setting', + doc => 'this is a setting', + type => 'integer', + category => 'general', + default => 0 + }), + 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' + ); +}; -# 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 +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 }; -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'; - -# 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'; - -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_deeply($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_deeply($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_deeply($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'); - -# 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'); - -# 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'); - -# 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'; - -# 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} }); done_testing(); diff --git a/t/db/003_users.t b/t/db/003_users.t index ace345db..147c5714 100644 --- a/t/db/003_users.t +++ b/t/db/003_users.t @@ -2,309 +2,269 @@ # 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 lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - -use Test::More; -use Test::Exception; -use Clone qw/clone/; - -use YAML qw/LoadFile/; -use DateTime::Format::Strptime; -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 $strp = DateTime::Format::Strptime->new(pattern => '%F', on_error => 'croak'); - -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}; +use Test2::V0; + +# 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 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/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs cleanUndef/; + +my $ww3_dir = curfile->dirname->dirname->dirname; + +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 @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); } - 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', + + @users = sort { $a->{username} cmp $b->{username} } @users; + + # Get a list of all users. + my @users_from_db = $users_rs->getAllGlobalUsers; + for my $user (@users_from_db) { + removeIDs($user); + cleanUndef($user); } -); -my @all_students = sort { $a->{username} cmp $b->{username} } @students; + @users_from_db = sort { $a->{username} cmp $b->{username} } @users_from_db; + is(\@users_from_db, \@users, 'getUsers: all users'); -# Get a list of all users. -my @users_from_db = $users_rs->getAllGlobalUsers; -for my $user (@users_from_db) { + # 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_deeply(\@all_students, \@users_from_db, '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'); - -# 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'); - -# Get one user that does not exist -throws_ok { - $user = $users_rs->getGlobalUser(info => { user_id => -9 }); -} -'DB::Exception::UserNotFound', 'getUser: undefined user_id'; - -throws_ok { - $user = $users_rs->getGlobalUser(info => { username => 'non_existent_user' }); -} -'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'; - -# Add one user -$user = { - username => 'wiggam', - last_name => 'Wiggam', - first_name => 'Clancy', - email => 'wiggam@springfieldpd.gov', - student_id => '', - is_admin => false, -}; + is($user, $users[0], 'getUser: by username'); -my $new_user = $users_rs->addGlobalUser(params => $user); -removeIDs($new_user); -is_deeply($user, $new_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_deeply($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' - } + # 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' ); -} -'DB::Exception::ParametersNeeded', 'addUser: wrong user_info sent'; -# Check that adding an invalid field ignores that field. + is( + dies { $user = $users_rs->getGlobalUser(info => { username => 'non_existent_user' }); }, + check_isa('DB::Exception::UserNotFound'), + 'getUser: undefined username' + ); -my $selma_params = { - username => 'selma', - invalid_field => 1 -}; + # 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' + ); -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_deeply($selma_params, $selma, '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' - } + # 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' ); -} -'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_deeply($user2, $added_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( - 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'); - -# 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'; - -# 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'; - -throws_ok { - $users_rs->updateGlobalUser(info => { user_id => -5 }, params => $updated_user); -} -'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' + # 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' }); }, - 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'); - -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'); - -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'); - -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.'); - -# 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'; - -throws_ok { - $user = $users_rs->deleteGlobalUser(info => { user_id => -3 }); -} -'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'] - } -); -@students = loadCSV("$main::ww3_dir/t/db/sample_data/students.csv"); + 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' + ); -my @user_courses_from_csv = grep { $_->{username} eq 'lisa' } @students; + # 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' + ); -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 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); } - 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; + my $courses = decode_json($ww3_dir->child('t/db/sample_data/courses.json')->slurp); -is_deeply(\@user_courses, \@user_courses_from_csv, 'getUserCourses: get all courses for a given user'); + 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}; + } + } -## try to get a list of course from a non-existent user + # 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; -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(\@user_courses, \@user_courses_from_json, 'getUserCourses: get all courses for a given 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) { - removeIDs($user); - 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'); + # 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 b79c4aa9..dafc5efb 100644 --- a/t/db/004_course_users.t +++ b/t/db/004_course_users.t @@ -2,375 +2,373 @@ # This tests the basic database CRUD functions of course users. -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 Test::More; -use Test::Exception; -use YAML::XS qw/LoadFile/; +use Mojo::File qw/curfile/; use Clone qw/clone/; -use Mojo::JSON qw/true 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"; -$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} = 0; - $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_deeply(\@precalc_students, \@precalc_users_from_db, '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'; - -# 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'; - -# 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'; - -# Test getUser - -my $user = $user_rs->getCourseUser( - info => { - course_name => 'Precalculus', - username => $precalc_students[0]->{username} - }, - merged => 1 -); -removeIDs($user); - -is_deeply($precalc_students[0], $user, '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'; - -# 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'; - -# 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'; - -# 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 => 0 -}; +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; -my $course_user_params = { - username => 'quimby', - role => 'student', - course_user_params => {}, - section => undef, - recitation => undef, -}; + # 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; -$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_deeply($course_user_params, $user, '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_deeply($quimby_params, $quimby_db, '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"; - -# 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'; - -# 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'; - -# addCourseUser: add a user with undefined parameters -throws_ok { - $user_rs->addCourseUser( - info => { - course_name => 'Topology', - username => 'quimby', - }, - params => { - role => 'student', - undefined_field => 1 - } + 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' ); -} -'DBIx::Class::Exception', 'addCourseUser: an undefined field is passed in'; -# Add a user with undefined course user parameters. -throws_ok { - $user_rs->addCourseUser( + # Test getUser + + my $user = $user_rs->getCourseUser( info => { - course_name => 'Topology', - username => 'quimby' + course_name => 'Precalculus', + username => $precalc_students[0]->{username} }, - params => { - role => 'student', - course_user_params => { - this_is_not_valid => 1 - } - } + merged => 1 ); -} -'DB::Exception::InvalidField', 'addCourseUser: an undefined parameter is set'; + removeIDs($user); -# Add a user with nonvalid fields -throws_ok { - $user_rs->addCourseUser( - info => { - course_name => 'Topology', - username => 'quimby' + 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' }); }, - params => { - role => 'student', - course_user_params => { - useMathQuill => 0 - } - } + check_isa('DB::Exception::UserNotFound'), + 'getCourseUser: undefined user' ); -} -'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' + # 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' }) }, - params => { - role => 'cop' - } + check_isa('DB::Exception::UserNotFound'), + 'addCourseUser: try to add a non-existent user to a course' ); -} -'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}; -} + # 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' + ); -my $user_from_db = $user_rs->updateCourseUser( - info => { course_name => 'Arithmetic', username => 'quimby' }, - params => $updated_user -); + # 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' + ); -removeIDs($user_from_db); -is_deeply($course_user_params, $user_from_db, 'updateCourseUser: update a single user in an existing course.'); + # 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' + ); -# 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 + # 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' ); -} -'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' }, + # 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 ); -} -'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 + 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} ); -} -'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 } + + # 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' ); -} -'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 - } - } + # 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.' ); -} -'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' - } - } + # 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' ); -} -'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. + # 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' + ); -SKIP: { - skip 'delete added users', 5 if $dont_delete_users; + # 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' + ); - my $deleted_course_user = $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username => 'quimby' }); + # 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_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' }); + my $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'), + q{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.'; -} - -# 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_deeply(\@precalc_students, \@precalc_users_from_db, - 'check: ensure that the precalc users in the database is restored.'); + is( + dies { + $user_rs->deleteCourseUser(info => { course_name => 'Arithmetic', username_name => 'bart' }); + }, + check_isa('DB::Exception::ParametersNeeded'), + 'deleteUser: the incorrect information is passed in.' + ); +}; done_testing; diff --git a/t/db/005_hwsets.t b/t/db/005_hwsets.t index ef79c073..c492b334 100644 --- a/t/db/005_hwsets.t +++ b/t/db/005_hwsets.t @@ -2,455 +2,389 @@ # This tests the basic database CRUD functions of problem sets. -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 Test::More; -use Test::Exception; -use Clone qw/clone/; -use YAML::XS qw/LoadFile/; -use DateTime::Format::Strptime; -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 } -); - -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'); -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 Test2::V0; + +# 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/encode_json decode_json true false/; + +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_deeply(\@all_problem_sets, \@problem_sets_from_db, '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_deeply(\@all_precalc_sets, \@precalc_sets_from_db, '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_deeply(\@precalc_hw, \@precalc_hw_from_db, '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'); - -# 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'; - -# 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'; - -# 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_deeply($new_set_params, $new_set, "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' -}; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set2 + + # 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); } + } + + @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' ); -} -'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' -}; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set3 - } + + # 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' ); -} -'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' -}; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set4 - } + + # 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' ); -} -'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' -}; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set5 - } + + # 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.' ); -} -'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 => {} -}; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set6 - } + + # 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.' ); -} -'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 } -}; -throws_ok { - $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_set7 - } + + # 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' ); -} -'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' } - } + + # 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' ); -} -'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 } - } + + # 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.' ); -} -'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 } - } + + # 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' ); -} -'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 - } - } -); -removeIDs($updated_set); -delete $new_set_params->{type}; -is_deeply($new_set_params, $updated_set, '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_deeply($new_set_params, $updated_set, "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_deeply($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 } + + # 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' ); -} -'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 } } + + # 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' ); -} -'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 - } - } + + # 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' ); -} -'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'); - -# Try deleting a set with invalid course_name -throws_ok { - $problem_set_rs->deleteProblemSet( - info => { - course_name => 'Not a course', - set_name => 'HW #1' - } + + # 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} } ); -} -'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' - } + 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' ); -} -'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_deeply(\@all_problem_sets, \@problem_sets_from_db, 'check: ensure that the problem_sets table is restored.'); + + # 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 7ca3abc8..623b987f 100644 --- a/t/db/006_quizzes.t +++ b/t/db/006_quizzes.t @@ -2,433 +2,365 @@ # 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 lib "$main::ww3_dir/lib"; -use lib "$main::ww3_dir/t/lib"; - -use Test::More; -use Test::Exception; -use YAML::XS qw/LoadFile/; -use Clone qw/clone/; -use DateTime::Format::Strptime; -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 } -); - -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'); -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"); -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_deeply(\@precalc_quizzes, \@precalc_quizzes_from_db, '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'); - -# 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'; - -# 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'; - -# 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' -}; +use Test2::V0; -my $new_quiz = $problem_set_rs->addProblemSet( - params => { - course_name => 'Precalculus', - %$new_quiz_params - } -); +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -removeIDs($new_quiz); -## add the default set_visible field -$new_quiz_params->{set_visible} = false; -is_deeply($new_quiz, $new_quiz_params, "addQuiz: add a new quiz"); +use Mojo::File qw/curfile/; +use Mojo::JSON qw/decode_json true false/; -# 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' +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); } + } + + # 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}; + } + @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' ); -} -'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'; + # 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' + ); -# Try to add a quiz without specifying the name. -throws_ok { - $problem_set_rs->addProblemSet( + # 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', - set_type => 'QUIZ', - set_visible => true, + %$new_quiz_params } ); -} -'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, - } - } + 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' ); -} -'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, - } - } + # 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' ); -} -'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 - } - } + # 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' ); -} -'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 - } - } + # 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' ); -} -'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, - } - } + # 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' ); -} -'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} = 0; -$new_quiz->{set_params} = {}; -removeIDs($updated_quiz); -is_deeply($new_quiz, $updated_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_deeply($new_quiz, $updated_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} = clone($updated_params->{set_dates}); -is_deeply($new_quiz, $updated_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' + + # 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 } + } + ); }, - params => { - nonexistent_field => 1 - } + check_isa('DB::Exception::FieldsNeeded'), + 'addQuiz: try to add a quiz with a missing required date fields' ); -} -'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' + # 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 } + } + ); }, - params => { - set_params => { - show_hint => 1 - } - } + check_isa('DB::Exception::InvalidField'), + 'addQuiz: try to add a quiz with an undefined date field' ); -} -'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' + # 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, } + } + ); }, - params => { - set_params => { - timed => 'yes' - } - } + 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 ); -} -'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( + $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 => { - set_dates => { - reduced_scoring => 1000 - } - } + 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' ); -} -'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' + # 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 } } + ); }, - params => { - set_dates => { - open => 50, - due => 40 - } - } + check_isa('DB::Exception::InvalidField'), + 'updateQuiz: try to update a quiz with an undefined parameter' ); -} -'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' - } + # 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' ); -} -'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' - } + # 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' ); -} -'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' - } + # 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' ); -} -'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( + # 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_id => 99999 + set_name => 'Quiz #9' } ); -} -'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_deeply($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_deeply(\@precalc_quizzes, \@precalc_quizzes_from_db, 'check: ensure that the quizzes have been restored.'); + removeIDs($deleted_quiz); + is($deleted_quiz, $new_quiz, 'delete Quiz: successfully delete a quiz'); +}; done_testing; diff --git a/t/db/007_user_set.t b/t/db/007_user_set.t index fb5abff4..1dea3db6 100644 --- a/t/db/007_user_set.t +++ b/t/db/007_user_set.t @@ -2,860 +2,704 @@ # This tests the basic database CRUD functions of problem sets of type quiz. -use warnings; -use strict; -use feature 'say'; +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 DateTime::Format::Strptime; -use Test::More; +use Mojo::File qw/curfile/; use Clone qw/clone/; -use Test::Exception; - -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 $strp = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); - -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); + # 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); + } + } -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 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); + } } -); -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 @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}); + } + } + } -my @merged_user_sets = @{ clone(\@all_user_sets) }; + my @merged_user_sets = @{ clone(\@all_user_sets) }; -# Merge the 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]; -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}; + } - # 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}; - } + # 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}; + } - # 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}; + $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}); } - $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}); -} - -# Get all user sets for a given user in a course. -my @all_user_sets_from_db = $user_set_rs->getAllUserSets(); + # 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(); -for my $set (@all_user_sets_from_db) { - removeIDs($set); - for my $key (keys %{$set}) { - delete $set->{$key} unless defined $set->{$key}; + for my $set (@all_user_sets_from_db) { + removeIDs($set); + cleanUndef($set); } -} -# 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; + # 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; -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); + my @merged_sets_from_db = $user_set_rs->getAllUserSets(merged => 1); -for my $merged_set (@merged_sets_from_db) { - removeIDs($merged_set); -} + for my $merged_set (@merged_sets_from_db) { + removeIDs($merged_set); + } + + # 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; -# 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; + is(\@merged_sets_from_db, \@merged_user_sets, 'getAllUserSets: get all merged sets for all courses'); -is_deeply(\@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. -# Get all user set for a given user in a course. + my @user_sets_for_one_user = + grep { $_->{course_name} eq 'Precalculus' && $_->{username} eq 'homer' } @all_user_sets; -my @user_sets_for_one_user = grep { $_->{course_name} eq 'Precalculus' && $_->{username} eq 'homer' } @all_user_sets; + my @user_sets_from_db = + $user_set_rs->getUserSetsForUser(info => { course_name => $course->course_name, username => 'homer' }); -my @user_sets_from_db = $user_set_rs->getUserSetsForUser( - info => { - course_name => $course->course_name, - username => 'homer' + for my $user_set (@user_sets_from_db) { + removeIDs($user_set); + delete $user_set->{set_visible} unless defined($user_set->{set_visible}); } -); -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; -# 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'); -is_deeply(\@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 -# Get all merged sets for a given user in a course + my @merged_sets_for_one_user = + grep { $_->{course_name} eq 'Precalculus' && $_->{username} eq 'homer' } @merged_user_sets; -my @merged_sets_for_one_user = - grep { $_->{course_name} eq 'Precalculus' && $_->{username} eq 'homer' } @merged_user_sets; + @merged_sets_from_db = $user_set_rs->getUserSetsForUser( + info => { course_name => $course->course_name, username => 'homer' }, + merged => 1 + ); -@merged_sets_from_db = $user_set_rs->getUserSetsForUser( - info => { - course_name => $course->course_name, - username => 'homer' - }, - merged => 1 -); + for my $merged_set (@merged_sets_from_db) { + removeIDs($merged_set); + } -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'); -is_deeply(\@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; -my @user_sets_for_one_set = grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @all_user_sets; + @user_sets_from_db = + $user_set_rs->getUserSetsForSet(info => { course_name => 'Precalculus', set_name => 'HW #1' }); -@user_sets_from_db = $user_set_rs->getUserSetsForSet(info => { course_name => 'Precalculus', set_name => 'HW #1' }); + for my $user_set (@user_sets_from_db) { + removeIDs($user_set); + delete $user_set->{set_visible} unless defined($user_set->{set_visible}); + } -for my $user_set (@user_sets_from_db) { - removeIDs($user_set); - delete $user_set->{set_visible} unless defined($user_set->{set_visible}); -} + is(\@user_sets_from_db, \@user_sets_for_one_set, 'getUserSets: get all user sets for a set in a course'); -is_deeply(\@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 -# get all merged user sets for a given set in a course + my @merged_sets_for_one_set = + grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @merged_user_sets; -my @merged_sets_for_one_set = - grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @merged_user_sets; + @merged_sets_from_db = $user_set_rs->getUserSetsForSet( + info => { course_name => 'Precalculus', set_name => 'HW #1' }, + merged => 1 + ); -@merged_sets_from_db = $user_set_rs->getUserSetsForSet( - info => { course_name => 'Precalculus', set_name => 'HW #1' }, - merged => 1 -); + for my $user_set (@merged_sets_from_db) { + removeIDs($user_set); + } -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'); - -# 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'; - -# 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'; - -# 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'; - -# 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_deeply($user_set_from_csv, $user_set, '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_deeply($merged_set_from_csv, $merged_set, '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(\@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-existent user. -throws_ok { - $user_set_rs->getUserSet( - info => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1' - } + # 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' ); -} -'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' - } + # 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' ); -} -'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' - } + # 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' ); -} -'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_deeply($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]); + # 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] + ); -$hw_set1->{username} = 'frink'; + removeIDs($user_set); + delete $user_set->{set_visible} unless defined($user_set->{set_visible}); -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_deeply($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 => {} -}; + is($user_set, $user_set_from_json, 'getUserSet: get a user set from a course'); -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_deeply($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); + # Get a merged UserSet -# The bad_field isn't returned -delete $user_set_params3->{bad_field}; + 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] + ); -# and need to explicitly set the type -$user_set_params3->{set_type} = 'HW'; + my $merged_set = $user_set_rs->getUserSet(info => $info, merged => 1); + removeIDs($merged_set); -is_deeply($user_set3, $user_set_params3, 'addUserSet: add a user set with a bad field'); + is($merged_set, $merged_set_from_json, 'getUserSet: get a merged set from a course'); -# 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' - } + # 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' ); -} -'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' - } + # 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' ); -} -'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' - } + # 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' ); -} -'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' - } + # 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' ); -} -'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 + my $new_info = { username => 'otto', course_name => 'Precalculus', set_name => 'HW #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); - -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_deeply($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 - } - } - ) -} -'DB::Exception::InvalidField', 'addUserSet: try to add a new user set with an undefined parameter'; + my $new_user_set = $user_set_rs->addUserSet(params => $new_info); + removeIDs($new_user_set); + cleanUndef($new_user_set); -## add a user set with a new date + # Set the other default parameters. + $new_info->{set_dates} = {}; + $new_info->{set_params} = {}; + $new_info->{set_version} = 0; + $new_info->{set_type} = 'HW'; -my $set_dates4 = { - open => 1, - reduced_scoring => 800, - due => 900, - answer => 1000 -}; + is($new_user_set, $new_info, 'addUserSet: add a new user set'); -my $otto_set_info4 = { - username => 'otto', - course_name => 'Precalculus', - set_name => 'HW #5', - set_version => 1 -}; + my $hw_set1 = + clone((grep { $_->{course_name} eq 'Precalculus' && $_->{set_name} eq 'HW #1' } @hw_sets)[0]); -my $otto_set_info5 = { - username => 'otto', - course_name => 'Precalculus', - set_name => 'HW #6', -}; + $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; -my $otto_set_params4 = { %$otto_set_info4, set_dates => $set_dates4 }; + is($new_merged_set, $hw_set1, 'addUserSet: add a new user set and check that it is merged correctly'); -my $user_set4 = $user_set_rs->addUserSet(params => $otto_set_params4); -removeIDs($user_set4); -cleanUndef($user_set4); + # Add a user set with a empty set of dates. -my $set_params4 = clone($otto_set_params4); -$set_params4->{set_type} = 'HW'; -$set_params4->{set_params} = {}; -$set_params4->{set_version} = 1; + my $new_user_params2 = + { username => 'ralph', course_name => 'Arithmetic', set_name => 'HW #3', set_dates => {} }; -is_deeply($user_set4, $set_params4, 'addUserSet: add a new user set with dates'); + my $new_user_set2 = $user_set_rs->addUserSet(params => $new_user_params2); + removeIDs($new_user_set2); + cleanUndef($new_user_set2); -# add a user with only override dates set. + # 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} = {}; -my $hw4 = $problem_set_rs->getProblemSet( - info => { - 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 - } -}; + is($new_user_set2, $new_user_params2, 'addUserSet: add a new user set with empty dates.'); -my $ralph_user_set = $user_set_rs->addUserSet(params => $ralph_set_info); -removeIDs($ralph_user_set); -cleanUndef($ralph_user_set); + # 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' + ); -# 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; + # 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' + ); -is_deeply($ralph_user_set, $ralph_set_info, 'addUserSet: add a new user with dates (some are missing).'); + # 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 bad date. -throws_ok { - $user_set_rs->addUserSet( - params => { - %$otto_set_info5, - set_dates => { - open => 100, - due => 9, - answer => 1000, - enable_reduced_scoring => false - } - } + # 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' ); -} -'DB::Exception::ImproperDateOrder', 'addUserSet: dates are out of order'; -throws_ok { - $user_set_rs->addUserSet( + 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_info5, - set_dates => { - open => 100, - due => 900, - answer => 800 - } + %$otto_set_info2, set_params => { description => 'This is the description for HW #2' } } ); -} -'DB::Exception::ImproperDateOrder', 'addUserSet: dates are out of order'; + 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 new user set and test that it is merged correctly. + # add a user set with a new date -my $otto_quiz_info = { - course_name => 'Precalculus', - set_name => 'Quiz #1', - username => 'otto' -}; + my $set_dates4 = { open => 1, reduced_scoring => 800, due => 900, answer => 1000 }; -# Then add a new user set and test that it is merged correctly. + my $otto_set_info4 = + { username => 'otto', course_name => 'Precalculus', set_name => 'HW #5', set_version => 1 }; -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] -); + my $otto_set_info5 = { username => 'otto', course_name => 'Precalculus', set_name => 'HW #6' }; -$merged_set1->{username} = $otto_quiz_info->{username}; + my $otto_set_params4 = { %$otto_set_info4, set_dates => $set_dates4 }; -my $new_dates = { - due => $merged_set->{set_dates}->{answer} + 1000, - answer => $merged_set->{set_dates}->{answer} + 2000 -}; + my $user_set4 = $user_set_rs->addUserSet(params => $otto_set_params4); + removeIDs($user_set4); + cleanUndef($user_set4); -$merged_set1->{set_dates}->{due} = $new_dates->{due}; -$merged_set1->{set_dates}->{answer} = $new_dates->{answer}; + my $set_params4 = clone($otto_set_params4); + $set_params4->{set_type} = 'HW'; + $set_params4->{set_params} = {}; + $set_params4->{set_version} = 1; -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_set4, $set_params4, 'addUserSet: add a new user set with dates'); -is_deeply($merged_set1, $user_set_to_merge, 'addUserSet: adding a user set with dates to check merging'); + # add a user with only override dates set. -## Check that adding a user set that are out of order with the problem sets throws an error. + my $hw4 = $problem_set_rs->getProblemSet(info => { course_name => 'Precalculus', set_name => 'HW #4' }); -throws_ok { - $user_set_rs->addUserSet( - params => { - %$otto_set_info5, - set_dates => { - due => 1609595640, # this is after the problem set answer date. - } + 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, }, - merged => 1 + 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' ); -} -'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 + 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. -# Currently, this is stripped out if hide_hint is 0. Doe we want to check for this? + my $otto_quiz_info = { course_name => 'Precalculus', set_name => 'Quiz #1', username => 'otto' }; -throws_ok { - $user_set_rs->addUserSet( - params => { - %$otto_set_info5, - set_params => { - hide_hint => 1 - } - }, - merged => 1 + # 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] ); -} -'DB::Exception::InvalidParameter', 'addUserSet: boolean valid should be JSON boolean'; -# Update User Set -# Get the user set for $otto_set_info2. + $merged_set1->{username} = $otto_quiz_info->{username}; -my $otto_quiz = $user_set_rs->getUserSet(info => $otto_quiz_info); -removeIDs($otto_quiz); + my $new_dates = { + due => $merged_set->{set_dates}{answer} + 1000, + answer => $merged_set->{set_dates}{answer} + 2000 + }; -# Update the dates -my $updated_dates = { - due => $otto_quiz->{set_dates}->{due} + 100, - answer => $otto_quiz->{set_dates}->{due} + 1000 -}; + $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' + ); -$otto_quiz->{set_dates}->{due} = $updated_dates->{due}; -$otto_quiz->{set_dates}->{answer} = $updated_dates->{answer}; + # 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' + ); -my $updated_user_quiz = $user_set_rs->updateUserSet(info => $otto_quiz_info, params => $otto_quiz); -removeIDs($updated_user_quiz); + # Update User Set -is_deeply($updated_user_quiz, $otto_quiz, 'updateUserSet: update the dates'); + my $otto_quiz = $user_set_rs->getUserSet(info => $otto_quiz_info); + removeIDs($otto_quiz); -# 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_deeply($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; + # Update the dates + my $updated_dates = { + due => $otto_quiz->{set_dates}{due} + 100, + answer => $otto_quiz->{set_dates}{due} + 1000 + }; -is_deeply($otto_quiz, $updated_user_set3, 'updateUserSet: update the set visibility'); + $otto_quiz->{set_dates}{due} = $updated_dates->{due}; + $otto_quiz->{set_dates}{answer} = $updated_dates->{answer}; -# 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'; + my $updated_user_quiz = $user_set_rs->updateUserSet(info => $otto_quiz_info, params => $otto_quiz); + removeIDs($updated_user_quiz); -# Try updating an invalid date. -throws_ok { - $user_set_rs->updateUserSet( + 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_dates => { - open => 1, - closed => 2 - } - } + params => { set_params => { problem_randorder => true } } ); -} -'DB::Exception::InvalidField', 'updateUserSet: try to update an invalid date field'; + removeIDs($updated_user_set2); + $otto_quiz->{set_params}{problem_randorder} = true; + is($updated_user_set2, $otto_quiz, 'updateUserSet: update the params'); -# Test with out of order dates. -throws_ok { - $user_set_rs->updateUserSet( + # Update a valid field + my $updated_user_set3 = $user_set_rs->updateUserSet( info => $otto_quiz_info, - params => { - set_dates => { - open => 100, - due => 2, - answer => 200 - } - } + params => { set_visible => true } ); -} -'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 - } - } + 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' ); -} -'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'; - -# 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_deeply($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_deeply($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"); - -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' } ] - } -); -for my $u (@otto_user_sets) { - $u->delete; -} + # 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' + ); -# Check that the user_sets db table is restored. -@all_user_sets_from_db = $user_set_rs->getAllUserSets(); + # 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' + ); -for my $set (@all_user_sets_from_db) { - removeIDs($set); - cleanUndef($set); - for my $key (keys %{$set}) { - delete $set->{$key} unless defined $set->{$key}; - } -} + # 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' + ); -# 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; + # 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' + ); -is_deeply(\@all_user_sets_from_db, \@all_user_sets, 'check: ensure that the user sets are restored.'); + # 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 dc731a66..a0bfd264 100644 --- a/t/db/008_problem_pools.t +++ b/t/db/008_problem_pools.t @@ -2,311 +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 Test::More; -use Test::Exception; -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'] +use Test2::V0; + +# 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} + } + ); + } + } } -); - -# 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_deeply(\@problem_pools_from_file, \@problem_pools_from_db, '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'; -} + @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); + } + is(\@problem_pools_from_db, \@problem_pools_from_file, 'getAllProblemPools: find all problem pools'); -is_deeply(\@precalc_pools_from_file, \@precalc_pools, 'getProblemPools: get all problem pools from a single course'); + # 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'; + } + 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]; + # 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}; + 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'; + # 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. -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'; + # 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'; + # 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_deeply( - { - pool_name => $pool_name - }, - $pool2, - 'addProblemPool: add a new problem pool to a course' -); - -# Try to add a pool that already exists. -throws_ok { - $problem_pool_rs->addProblemPool( + my $pool2 = $problem_pool_rs->addProblemPool( params => { - course_name => 'Arithmetic', - pool_name => 'adding fractions' + course_name => $course_name, + pool_name => $pool_name } ); -} -'DB::Exception::PoolAlreadyInCourse', 'addProblemPool: pool already exists'; + removeIDs($pool2); -# 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' - } + 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' ); -} -'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', }; + # 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' + ); -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'; + # Update an existing problem pool. + my $updated_pool = { pool_name => 'subtracting fractions with like denominators', }; -removeIDs($updated_pool_from_db); -$updated_pool->{course_name} = 'Arithmetic'; + 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_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. + # 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 + # 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' ); -} -'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 + # 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' ); -} -'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]; + # 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} + } + ); -my $pool_problem2 = $problem_pool_rs->getPoolProblem(info => $prob2); + # Get a PoolProblem (a problem within a ProblemPool). + my $prob2 = $pool_problems_from_file[0]; -is($prob2->{library_id}, $pool_problem2->{library_id}, 'getPoolProblem: get a single problem from a problem pool'); + my $pool_problem2 = $problem_pool_rs->getPoolProblem(info => $prob2); -# Get a random PoolProblem. -my $random_prob = $problem_pool_rs->getPoolProblem( - info => { - course_name => $prob2->{course_name}, - pool_name => $prob2->{pool_name} - } -); + is( + $prob2->{library_id}, + $pool_problem2->{library_id}, + 'getPoolProblem: get a single problem from a problem pool' + ); -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; + # Get a random PoolProblem. + my $random_prob = $problem_pool_rs->getPoolProblem( + info => { + course_name => $prob2->{course_name}, + pool_name => $prob2->{pool_name} + } + ); -ok(scalar(@arr) == 1, 'getPoolProblem: get a random problem from a problem pool'); + 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; -# 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 }); + ok(scalar(@arr) == 1, 'getPoolProblem: get a random problem from a problem pool'); -is( - $prob_to_add->{params}->{library_id}, - $added_problem->{params}->{library_id}, - 'addProblemToPool: adding a problem to an existing 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 }); -# 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 - } + is( + $prob_to_add->{params}{library_id}, + $added_problem->{params}{library_id}, + 'addProblemToPool: adding a problem to an existing pool.' ); -} -'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 - } + # 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' ); -} -'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}; + # 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' + ); -my $updated_library_id = 2839; + # 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_pool_problem = $problem_pool_rs->updatePoolProblem( - info => $course_pool_problem_info, - params => { params => { library_id => $updated_library_id } } -); + my $updated_library_id = 2839; -is( - $updated_library_id, - $updated_pool_problem->{params}->{library_id}, - 'updatePoolProblem: update an existing problem in an existing pool.' -); + my $updated_pool_problem = $problem_pool_rs->updatePoolProblem( + info => $course_pool_problem_info, + params => { params => { library_id => $updated_library_id } } + ); -# 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} + 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 } } + ); }, - params => { - params => { - library_id => $updated_library_id - } - } + check_isa('DB::Exception::CourseNotFound'), + 'updatePoolProblem: try to update a nonexisting course' ); -} -'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} + # 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 } + ); }, - params => { library_id => $updated_library_id } + check_isa('DB::Exception::PoolNotInCourse'), + 'updatePoolProblem: try to update a nonexisting pool' ); -} -'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 + # 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 } + ); }, - params => { library_id => $updated_library_id } + check_isa('DB::Exception::PoolProblemNotInPool'), + 'updatePoolProblem: try to update a nonexisting problem' ); -} -'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'); - -# 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_deeply(\@problem_pools_from_file, \@problem_pools_from_db, 'check: Ensure that the problem_pool table is restored.'); + + # 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 d001c650..986172f0 100644 --- a/t/db/009_problems.t +++ b/t/db/009_problems.t @@ -2,410 +2,308 @@ # This tests the basic database CRUD functions of problems. -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 Test::More; -use Test::Exception; +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/; +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; + +use BuildDB qw/addCourses addSets addProblems/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/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 $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); + addProblems($schema, $ww3_dir); + + my $problem_rs = $schema->resultset('SetProblem'); -my $problem_rs = $schema->resultset('SetProblem'); -my $problem_set_rs = $schema->resultset('ProblemSet'); + # 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} }); -# Load all problems from the CVS files. -my @problems_from_csv = loadCSV("$main::ww3_dir/t/db/sample_data/problems.csv"); + # Separate out precalculus problems. + push(@precalc_problems, $_) if $course_info->{course_name} eq 'Precalculus'; -# Filter out precalc problems -my @precalc_problems = grep { $_->{course_name} eq 'Precalculus' } @problems_from_csv; + # Separate out precalculus 'HW #1' problems. + push(@precalc_problems1, $_) + if $course_info->{course_name} eq 'Precalculus' && $set_info->{set_name} eq 'HW #1'; + } + } + } -# Filter out 'HW #1' -my @precalc_problems1 = grep { $_->{set_name} eq 'HW #1' } @precalc_problems; + # 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'); -for my $problem (@problems_from_csv) { - delete $problem->{course_name}; -} + # 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); + } + is(\@precalc_problems_from_db, \@precalc_problems, 'getSetProblems: get all problems from one course'); -my @all_problems = map { clone($_) } @problems_from_csv; + # 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' + ); -my @problems_from_db = $problem_rs->getGlobalProblems; -for my $problem (@problems_from_db) { - removeIDs($problem); -} + # 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' + ); -is_deeply(\@all_problems, \@problems_from_db, 'getGlobalProblems: get all problems'); + # Get all problems in one course from one set. + my @set_problems1 = $problem_rs->getSetProblems(info => { course_name => 'Precalculus', set_name => 'HW #1' }); -# 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); -} + for my $problem (@set_problems1) { + removeIDs($problem); + } -# Try to get all problems from a non existent course + is(\@set_problems1, \@precalc_problems1, 'getSetProblems: get all problems from one set'); -throws_ok { - $problem_rs->getProblems( - info => { - course_name => 'non existent course' - } + # 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' ); -} -'DB::Exception::CourseNotFound', 'getProblems: non-existent course'; -# Try to get all problems from a non-existent set_id + # 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' + ); -throws_ok { - $problem_rs->getProblems( + # Get a single problem from a course. + my $set_problem = $problem_rs->getSetProblem( info => { - course_id => 999999 + 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} } ); -} -'DB::Exception::CourseNotFound', 'getProblems: non-existent course_id'; - -for my $problem (@precalc_problems) { - delete $problem->{set_name}; -} + removeIDs($set_problem); -is_deeply(\@precalc_problems, \@precalc_problems_from_db, 'getSetProblems: get all problems from one course'); + my $expected_problem = { %{ $course_set_problems->[0]{sets}[0]{problems}[0] } }; # Copy the first problem -# Get all problems in one course from one set. -my @set_problems1 = $problem_rs->getSetProblems(info => { course_name => 'Precalculus', set_name => 'HW #1' }); + is($set_problem, $expected_problem, 'getSetProblem: get a single problem from a set in a given course'); -for my $problem (@set_problems1) { - removeIDs($problem); -} + # Add a problem to an existing set. + my $new_problem = { problem_number => 4, problem_params => { library_id => 13245, weight => 1 } }; -is_deeply(\@precalc_problems1, \@set_problems1, 'getSetProblems: get all problems from one set'); + my $prob1 = + $problem_rs->addSetProblem(params => { course_name => 'Precalculus', set_name => 'HW #1', %$new_problem }); -# 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'; + my $prob_id = $prob1->{set_problem_id}; + removeIDs($prob1); -# 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($prob1, $new_problem, 'addSetProblem: add a valid problem to a set'); -# 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} - } -); -removeIDs($set_problem); + # Add a problem and make sure the problem number is working. + # Determine the largest current problem number. -my $expected_problem = { %{ $problems_from_csv[0] } }; # Copy the first problem -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'); - -# 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 @hw1_probs = $problem_rs->getSetProblems(info => { course_name => 'Precalculus', set_name => 'HW #1' }); -my $prob_id = $prob1->{set_problem_id}; -removeIDs($prob1); + 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; } -is_deeply($new_problem, $prob1, '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_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, -}; + my $prob2 = { + 'problem_params' => { 'file_path' => 'path/to/problem.pg', 'weight' => 1 }, + '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' - } + # 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' ); -} -'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' - } + # 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' ); -} -'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' - } + # 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' ); -} -'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 - } + # 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' ); -} -'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 => {} - } + # 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." ); -} -'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 - } -}; -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}; + # 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 . -removeIDs($set_problem2); -delete $set_problem2->{problem_number}; + my $set_prob_params2 = { + course_name => 'Precalculus', + set_name => 'HW #1', + problem_params => { file_path => 'this_is_a_path', library_id => 123, weight => 1 } + }; -is_deeply($set_problem2, $set_prob_params2, 'addSetProblem: adding a problem with both file_path and library_id'); + 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}; -# Update a problem -my $updated_params = { - problem_number => 99, - problem_params => { - weight => 2 - } -}; + removeIDs($set_problem2); + delete $set_problem2->{problem_number}; -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($set_problem2, $set_prob_params2, 'addSetProblem: adding a problem with both file_path and library_id'); -is_deeply($all_params, $updated_problem, 'updateProblem: update a problem'); + # Update a problem + my $updated_params = { problem_number => 99, problem_params => { weight => 2 } }; -# 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' - } + 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 ); -} -'DB::Exception::CourseNotFound', 'updateSetProblem: try to update a problem to a non-existent course'; + removeIDs($updated_problem); -# 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($updated_problem, $all_params, 'updateProblem: update a problem'); -# Try to update a problem to a non-existent set -throws_ok { - $problem_rs->updateSetProblem( - info => { - course_name => 'Precalculus', - set_name => 'HW #99999' - } + # 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' ); -} -'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 - } + # 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' ); -} -'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, + # Try to update a problem to a non-existent set + is( + dies { + $problem_rs->updateSetProblem(info => { course_name => 'Precalculus', set_name => 'HW #99999' }); }, - params => { - problem_number => -9 - } + check_isa('DB::Exception::SetNotInCourse'), + 'updateSetProblem: try to update a problem to a non-existent set' ); -} -'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 - } - } + # 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' ); -} -'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_deeply($updated_problem, $deleted_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_deeply($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_deeply($deleted_problem3, $prob2, 'deleteSetProblem: delete another problem.'); + # 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' + ); -# 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); -} + # 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' + ); -is_deeply(\@all_problems, \@problems_from_db, 'The set_problems table is returned to its original state.'); + # 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); -# Ensure that the set_problems table is restored. -@problems_from_db = $problem_rs->getGlobalProblems; -for my $problem (@problems_from_db) { - removeIDs($problem); -} + is($deleted_problem, $updated_problem, 'deleteSetProblem: delete one problem in an existing set.'); -is_deeply(\@all_problems, \@problems_from_db, 'check: Ensure that the set_problems table is restored.'); + 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.'); +}; done_testing; diff --git a/t/db/010_user_problems.t b/t/db/010_user_problems.t index 83264eb5..9a7049e0 100644 --- a/t/db/010_user_problems.t +++ b/t/db/010_user_problems.t @@ -1,812 +1,791 @@ #!/usr/bin/env perl -# + # 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 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; + +# 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 Clone qw/clone/; -use YAML::XS qw/LoadFile/; - -use DB::Schema; -use TestUtils qw/loadCSV removeIDs loadSchema/; -use DB::Utils qw/updateAllFields/; - -# 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}); - } - # 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); + + # 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}; } - push(@merged_problems_from_csv, $problem); -} -# Get all user problems. + 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}); + } + } + } + } -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}; -} + # 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); + } + } + } -@all_user_problems_from_db = sort user_prob_sort_fxn @all_user_problems_from_db; + 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); + } -# 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_deeply(\@user_problems_from_csv, \@all_user_problems_from_db, - '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); + } -@merged_problems_from_db = sort user_prob_sort_fxn @merged_problems_from_db; + # For comparision, from the database needs to be a number. + $_->{status} = 0 + $_->{status} for (@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; + # For comparision, from the database needs to be a number. + $_->{status} = 0 + $_->{status} for (@merged_problems_from_db); -is_deeply(\@merged_problems_from_csv, \@merged_problems_from_db, 'getAllUserProblems: fetch all merged problems'); + @merged_problems_from_db = sort user_prob_sort_fcn @merged_problems_from_db; -# Get user problems from one course. + is(\@merged_problems_from_db, \@merged_problems_from_json, 'getAllUserProblems: fetch all merged problems'); -my @precalc_user_problems = grep { $_->{course_name} eq 'Precalculus' } @user_problems_from_csv; + # Get user problems from one course. + my @precalc_user_problems = grep { $_->{course_name} eq 'Precalculus' } @user_problems_from_json; -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_fxn @precalc_user_problems_from_db; - -is_deeply( - \@precalc_user_problems, - \@precalc_user_problems_from_db, - 'getUserProblems: get user problems from a single course.' -); - -# Get merged problems from one course. - -my @precalc_merged_problems = grep { $_->{course_name} eq 'Precalculus' } @merged_problems_from_csv; - -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; -} - -@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.' -); - -# 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 + 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; } -); -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_deeply($user_problem_from_csv, $user_problem, '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'; - -# 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'; -# Get a user problem from a set that doesn't exist. + @precalc_user_problems_from_db = sort user_prob_sort_fcn @precalc_user_problems_from_db; -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(\@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. + # Get merged problems from one course. + my @precalc_merged_problems = grep { $_->{course_name} eq 'Precalculus' } @merged_problems_from_json; -throws_ok { - $user_problem_rs->getUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 99 - } + my @precalc_merged_problems_from_db = $user_problem_rs->getUserProblems( + info => { course_name => 'Precalculus' }, + merged => 1 ); -} -'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 $user_problem1 = $user_problem_rs->addUserProblem( - params => { - %$problem_info1, seed => 7329 + for my $merged_problem (@precalc_merged_problems_from_db) { + removeIDs($merged_problem); + $merged_problem->{status} += 0; } -); -removeIDs($user_problem1); -$problem_info1->{seed} = 7329; -$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'); - -# 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 -}; -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_deeply($problem2, $user_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'; + @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; -# Attempt to add a UserProblem for a non-existent problem set. + is(\@precalc_merged_problems_from_db, + \@precalc_merged_problems, 'getUserProblems: get merged problems from a single course.'); -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'; - -# Attempt to add a UserProblem for a non-existent user. + # 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; -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1', - problem_number => 1 - } + 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] ); -} -'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 - } + 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' ); -} -'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 - } + # 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' ); -} -'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 - } + # 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' ); -} -'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 - } + # 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' ); -} -'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 + # Add a UserProblem to the database. + my $problem_info1 = + { course_name => 'Precalculus', set_name => 'HW #4', username => 'ned', problem_number => 1 }; -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'; + 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; -# Attempt to add a new User Problem with invalid library id + is($user_problem1, $problem_info1, 'addUserProblem: add a single user problem'); -throws_ok { - $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'; - -# Attempt to add a new User Problem with a bad problem weight + # 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 }; -throws_ok { - $user_problem_rs->addUserProblem( - params => { - course_name => 'Precalculus', - set_name => 'HW #4', - username => 'ned', - problem_number => 3, - problem_params => { - weight => -1 - } - } + my $user_problem2 = $user_problem_rs->addUserProblem( + params => { %$problem_info2, seed => 7329, problem_params => { weight => 2 } }, + merged => 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 + removeIDs($user_problem2); -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' - } - } + 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] ); -} -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 -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_deeply($user_problem1, $updated_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_deeply($problem2, $updated_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 - } + # Merge the two problems. + for my $key (qw/username seed status problem_version/) { + $problem2->{$key} = $user_problem2->{$key} if defined $user_problem2->{$key}; } -); -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_deeply($user_problem1, $updated_problem1a, '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 + $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 + } + ); }, - params => {} + check_isa('DB::Exception::CourseNotFound'), + 'addUserProblem: attempt to add a user problem to a nonexistent course' ); -} -'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 + # 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 + } + ); }, - params => {} + check_isa('DB::Exception::SetNotInCourse'), + 'addUserProblem: attempt to add a user problem for a non-existent problem set' ); -} -'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 + # 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 + } + ); }, - params => {} + check_isa('DB::Exception::UserNotFound'), + 'addUserProblem: attempt to add a user problem for a non-existent user' ); -} -'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 + # 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 + } + ); }, - params => {} + check_isa('DB::Exception::SetProblemNotFound'), + 'addUserProblem: attempt to add a user problem for a non-existent problem' ); -} -'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 + # 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 + } + ); }, - params => { - seed => -1234 - } + check_isa('DB::Exception::UserProblemExists'), + 'addUserProblem: attempt to add a user problem that already exists' ); -} -'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 + # 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 + } + ); }, - params => { - non_existent_field => 1 - } + check_isa('DB::Exception::InvalidParameter'), + 'addUserProblem: attempt to add a user problem with a bad seed' ); -} -'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 + # 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 + } + ); }, - params => { - non_existent_field => 1 - } + check_isa('DBIx::Class::Exception'), + 'addUserProblem: attempt to add a user problem with a non existent field' ); -} -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 + # 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 + } + ); }, - params => { - problem_params => { - library_id => -1234 - } - } + qr/No such column 'non_existent_field'/, + 'addUserProblem: attempt to add a user problem with a non existent field' ); -} -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 + # 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 } + } + ); }, - params => { - problem_params => { - weight => -1 - } - } + qr/library_id is not valid/, + 'addUserProblem: attempt to add a user problem with with invalid library id' ); -} -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 + # 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 } + } + ); }, - params => { - problem_params => { - problem_pool_id => 'fred' - } - } + qr/weight is not valid/, + 'addUserProblem: attempt to add a user problem with a bad problem weight' ); -} -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; -} + # 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_deeply(\@course_user_problems_from_csv, - \@user_problems, '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_deeply($user_problem1, $user_problem_to_delete, '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_deeply($problem2, $user_problem_to_delete2, - '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' + ); -throws_ok { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'course doesn\'t exist', - username => 'homer', - set_name => 'HW #1', - problem_number => 1 + # 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 } + ); }, - params => {} + check_isa('DBIx::Class::Exception'), + 'updateUserProblem: attempt to update a user problem with a non existent field' ); -} -'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. + # 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' + ); -throws_ok { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #999', - problem_number => 1 + # 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 } } + ); }, - params => {} + qr/library_id is not valid/, + 'updateUserProblem: attempt to update a user problem with with invalid library id' ); -} -'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. + # 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' + ); -throws_ok { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'Precalculus', - username => 'non_existent_user', - set_name => 'HW #1', - problem_number => 1 + # 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' } } + ); }, - params => {} + qr/problem_pool_id is not valid/, + 'updateUserProblem: attempt to update a user problem with a bad problem_pool_id' ); -} -'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. + # 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; -throws_ok { - $user_problem_rs->deleteUserProblem( - info => { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #1', - problem_number => 99 + 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 => {} + ); }, - params => {} + check_isa('DB::Exception::CourseNotFound'), + 'deleteUserProblem: attempt to delete a user problem to a nonexistent course' ); -} -'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 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' + ); -@all_user_problems_from_db = sort user_prob_sort_fxn @all_user_problems_from_db; + # 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' + ); -is_deeply(\@user_problems_from_csv, \@all_user_problems_from_db, - 'check: Ensure that the set_problems table is restored.'); + # 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 6b599915..395f11c6 100644 --- a/t/db/011_attempts.t +++ b/t/db/011_attempts.t @@ -1,118 +1,71 @@ #!/usr/bin/env perl -# + # This tests the basic database CRUD functions of attempts. -# -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 Test::More; -use Test::Exception; -use Try::Tiny; -use YAML::XS qw/LoadFile/; - -use DB::Schema; -use TestUtils qw/loadCSV removeIDs loadSchema/; -use DB::Utils qw/updateAllFields/; - -# 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' - } - ] - } - } - } -); -$attempts->delete_all; +use Test2::V0; -# Add a few attempts for a give User Problem. +# This must occur after Test2::V0 is loaded as that package enables all warnings. +use Mojo::Base -signatures; -my $user_problem_info = { - course_name => 'Precalculus', - username => 'homer', - set_name => 'HW #2', - problem_number => 3 -}; +use Mojo::File qw/curfile/; -my $attempt_params1 = { - scores => [ 0, 1, 1 ], - answers => [ 'x', 'x^2', 'x^3' ], - comments => {} -}; +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; -my $attempt1 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params1 }); -removeIDs($attempt1); +use BuildDB qw/loadPermissions addCourses addUsers addSets addProblems addUserSets addUserProblems/; +use DBSubtest qw/dbSubtest/; +use TestUtils qw/removeIDs/; -is_deeply($attempt_params1, $attempt1, 'addAttempt: add an attempt'); +my $ww3_dir = curfile->dirname->dirname->dirname; -my $attempt_params2 = { - scores => [ 0, 1, 1 ], - answers => [ '2x', '3x^2', '4x^3' ], - comments => {} -}; +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 $attempt2 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params2 }); -removeIDs($attempt2); -is_deeply($attempt_params2, $attempt2, 'addAttempt: add another attempt'); + # 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 $attempt_params3 = { - scores => [ 0, 0, 0 ], - answers => [ '-2x', '2x^2', '4x^3' ], - comments => {} -}; + my $user_problem_rs = $schema->resultset('UserProblem'); + my $attempt_rs = $schema->resultset('Attempt'); + + # 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 $attempt_params1 = { scores => [ 0, 1, 1 ], answers => [ 'x', 'x^2', 'x^3' ], comments => {} }; -my $attempt3 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params3 }); -removeIDs($attempt3); -is_deeply($attempt_params3, $attempt3, 'addAttempt: add yet another attempt'); + my $attempt1 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params1 }); + removeIDs($attempt1); -my @all_attempts = $attempt_rs->getAttempts(info => $user_problem_info); -for my $attempt (@all_attempts) { - removeIDs($attempt); -} + is($attempt1, $attempt_params1, 'addAttempt: add an attempt'); -is_deeply([ $attempt_params1, $attempt_params2, $attempt_params3 ], - \@all_attempts, "getAttempts: get attempts for a user problem;"); + my $attempt_params2 = { scores => [ 0, 1, 1 ], answers => [ '2x', '3x^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 $attempt_params3 = { scores => [ 0, 0, 0 ], answers => [ '-2x', '2x^2', '4x^3' ], comments => {} }; + + my $attempt3 = $attempt_rs->addAttempt(params => { %$user_problem_info, %$attempt_params3 }); + removeIDs($attempt3); + 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( + \@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..3e6f6cc6 100644 --- a/t/db/012_set_versions.t +++ b/t/db/012_set_versions.t @@ -2,228 +2,211 @@ # This tests the basic functions related to versioning in user 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 Test::More; -use Test::Exception; -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}); -} + 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}; + } -# Get a user set from a course + my @merged_user_sets = @{ clone(\@all_user_sets) }; -my $user_set_info1 = { - username => 'bart', - course_name => 'Precalculus', - set_name => 'HW #2' -}; + # Merge the sets -my $user_set1 = $user_set_rs->getUserSet(info => $user_set_info1); -removeIDs($user_set1); -cleanUndef($user_set1); + 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]; -# Check that it is the same as that from the CSV file + # 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}; + } -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]; + # 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}; + } -# Make a new user set that has a set_version of 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}); + } -is_deeply($user_set1_from_csv, $user_set1, 'getUserSet: get a single user set from a course.'); + # Get a user set from a course -my $user_set1_v1_params = clone $user_set1; -$user_set1_v1_params->{set_version} = 1; + my $user_set_info1 = { + username => 'bart', + course_name => 'Precalculus', + set_name => 'HW #2' + }; -my $user_set1_v1 = $user_set_rs->addUserSet(params => { %$user_set_info1, %$user_set1_v1_params }); -removeIDs($user_set1_v1); -cleanUndef($user_set1_v1); + my $user_set1 = $user_set_rs->getUserSet(info => $user_set_info1); + removeIDs($user_set1); + cleanUndef($user_set1); -is_deeply($user_set1_v1, $user_set1_v1_params, "addUserSet: add a user set with version =1 "); + # 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]; -# Make a new user set that has a set_version of 2 + # Make a new user set that has a set_version of 1 -my $user_set1_v2_params = clone $user_set1_v1_params; -$user_set1_v2_params->{set_version} = 2; + is($user_set1, $user_set1_from_json, 'getUserSet: get a single user set from a course.'); -my $user_set1_v2 = $user_set_rs->addUserSet(params => { %$user_set_info1, %$user_set1_v2_params }); -removeIDs($user_set1_v2); -cleanUndef($user_set1_v2); + my $user_set1_v1_params = clone $user_set1; + $user_set1_v1_params->{set_version} = 1; -is_deeply($user_set1_v2, $user_set1_v2_params, "addUserSet: add a user set with version = 2."); + my $user_set1_v1 = $user_set_rs->addUserSet(params => { %$user_set_info1, %$user_set1_v1_params }); + removeIDs($user_set1_v1); + cleanUndef($user_set1_v1); -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($user_set1_v1, $user_set1_v1_params, "addUserSet: add a user set with version =1 "); -is_deeply( - \@all_user_set_versions, - [ $user_set1, $user_set1_v1, $user_set1_v2 ], - 'getUserSetVersions: get all versions of a user set.' -); + # Make a new user set that has a set_version of 2 -# clean up the created versioned user sets. + my $user_set1_v2_params = clone $user_set1_v1_params; + $user_set1_v2_params->{set_version} = 2; -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_deeply($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} - } -); + my $user_set1_v2 = $user_set_rs->addUserSet(params => { %$user_set_info1, %$user_set1_v2_params }); + removeIDs($user_set1_v2); + cleanUndef($user_set1_v2); -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_set1_v2, $user_set1_v2_params, "addUserSet: add a user set with version = 2."); -# Ensure that the user_sets table is restored. -my @all_user_sets_from_db = $user_set_rs->getAllUserSets(merged => 1); - -for my $set (@all_user_sets_from_db) { - removeIDs($set); - cleanUndef($set); -} - -# 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_deeply(\@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 79c06a5c..e3c4810d 100644 --- a/t/db/013_problem_versions.t +++ b/t/db/013_problem_versions.t @@ -2,181 +2,191 @@ # This tests the basic functions related to versioning in user 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 Test::More; -use Test::Exception; -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_deeply($user_problem1_from_csv, $user_problem1, '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_deeply($user_problem1_v2_params, $user_problem1_v2, "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_deeply($user_problem1_v3_params, $user_problem1_v3, "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_deeply( - \@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} - } -); -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'); - -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} + for my $user_problem (@all_user_problem_versions) { + removeIDs($user_problem); + $user_problem->{status} += 0; } -); -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( + \@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_deeply(\@user_problems_from_csv, \@all_user_problems_from_db, '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 d5b83338..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 rebuids 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/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/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 6efa4da1..bd073fd7 100644 --- a/t/mojolicious/001_login.t +++ b/t/mojolicious/001_login.t @@ -1,72 +1,70 @@ #!/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/; -use Test::More; -use Test::Mojo; -use YAML::XS qw/LoadFile/; +use lib curfile->dirname->dirname->sibling('lib')->to_string; +use lib curfile->dirname->sibling('lib')->to_string; -use YAML::XS qw/LoadFile/; +use DBSubtest qw/mojoDBSubtest/; -BEGIN { - use File::Basename qw/dirname/; - use Cwd qw/abs_path/; - $main::ww3_dir = abs_path(dirname(__FILE__)) . '/../..'; -} +mojoDBSubtest 'login routes' => sub ($t, $schema) { + # Deploy the database. + $schema->deploy({ add_drop_table => 1 }); -use lib "$main::ww3_dir/lib"; + # 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' } + }); -# 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); + # 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' + ); -my $t = Test::Mojo->new('WeBWorK3' => LoadFile($config_file)); + # 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 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 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 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 => 1, - user => { - email => 'lisa@google.com', - first_name => 'Lisa', - is_admin => 0, - 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 => 0, - 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 => 0, - 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 bf84f35a..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 Test::More; -use Test::Mojo; +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 = 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' => 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' => 1)->json_is('/user/user_id' => 1) - ->json_is('/user/is_admin' => 1); + $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' => 1); + $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' => 1); - -# 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_deeply($new_course, $t->tx->res->json, "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_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 + # 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' => false); - -$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..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 Test::More; -use Test::Mojo; +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 = Test::Mojo->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' => 1)->json_is('/user/user_id' => 1) - ->json_is('/user/is_admin' => 1); + # 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 => 0 -}; + 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_deeply($new_user, $new_user_from_db, '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_deeply($new_user, $t->tx->res->json, '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_deeply($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' => 0); -$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); + # 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' => 0); -$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 49f44f17..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 Test::More; -use Test::Mojo; +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 = Test::Mojo->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 ecb16982..cb094145 100644 --- a/t/mojolicious/005_problem_sets.t +++ b/t/mojolicious/005_problem_sets.t @@ -1,155 +1,123 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - -use Test::More; -use Test::Mojo; +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/; -use DateTime::Format::Strptime; - -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)); - -my $strp = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); - -# Connect to the database. -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); -my $t = Test::Mojo->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 be15d571..a10403eb 100644 --- a/t/mojolicious/006_quizzes.t +++ b/t/mojolicious/006_quizzes.t @@ -1,136 +1,100 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - -use Test::More; -use Test::Mojo; +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/; -use DateTime::Format::Strptime; - -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)); - -my $strp = DateTime::Format::Strptime->new(pattern => '%FT%T', on_error => 'croak'); - -# Connect to the database. -my $schema = DB::Schema->connect( - $config->{database_dsn}, - $config->{database_user}, - $config->{database_password}, - { quote_names => 1 } -); - -my $t = Test::Mojo->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 3f1839f4..56661c97 100644 --- a/t/mojolicious/007_review_sets.t +++ b/t/mojolicious/007_review_sets.t @@ -1,130 +1,97 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - -use Test::More; -use Test::Mojo; -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 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. -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 = Test::Mojo->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 Test2::V0; +use Mojo::Base -signatures; +use Test2::MojoX; +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]; + # 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 some things about this review set + # find the first review set in the course. + my $review_set1 = (grep { $_->{set_type} eq 'REVIEW' } @$all_problem_sets)[0]; -$t->get_ok("/webwork3/api/courses/4/sets/$review_set1->{set_id}")->status_is(200) - ->json_is('/set_name' => $review_set1->{set_name}); + # test some things about this review set -# Test that booleans are returned correctly. + $t->get_ok("/webwork3/api/courses/4/sets/$review_set1->{set_id}")->status_is(200) + ->json_is('/set_name' => $review_set1->{set_name}); -$review_set1 = $t->tx->res->json; + # Test that booleans are returned correctly. -# 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'); + $review_set1 = $t->tx->res->json; -# Make a new quiz - -my $new_review_set_params = { - set_name => 'Review #20', - set_type => 'REVIEW', - set_params => { - can_retake => true, - }, - set_dates => { - open => 100, - closed => 200 - } -}; + # 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->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; -$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 + # 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 72fbb83c..0d2cba52 100644 --- a/t/mojolicious/008_problems.t +++ b/t/mojolicious/008_problems.t @@ -1,183 +1,158 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - -use Test::More; -use Test::Mojo; -use Mojo::JSON qw/true false/; - -use DateTime::Format::Strptime; - -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 = 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); - -# 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 Test2::V0; +use Mojo::Base -signatures; +use Test2::MojoX; +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) -$t->get_ok('/webwork3/api/courses/4/sets')->status_is(200)->content_type_is('application/json;charset=UTF-8'); + # 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'); -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_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. + # 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' => 0); -$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); + # 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' => 0); -$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); + # 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' => 0); -$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 7066715a..2ba938f3 100644 --- a/t/mojolicious/009_user_problems.t +++ b/t/mojolicious/009_user_problems.t @@ -1,248 +1,219 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - -use Test::More; -use Test::Mojo; -use Mojo::JSON qw/true false/; - -use DateTime::Format::Strptime; - -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 = 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); - -# 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 Test2::V0; +use Mojo::Base -signatures; +use Test2::MojoX; +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_deeply(\@arith_problems, $problems_from_db, '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_deeply(\@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_deeply(\@ralph_user_problems_from_file, - $ralph_user_problems, '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' => 0); -$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' => 0); -$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' => 0); -$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 83e3e992..4fd9fcc2 100644 --- a/t/mojolicious/010_problem_pools.t +++ b/t/mojolicious/010_problem_pools.t @@ -1,206 +1,163 @@ #!/usr/bin/env perl -use Mojo::Base -strict; - -use Test::More; -use Test::Mojo; - -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 Test2::V0; +use Mojo::Base -signatures; +use Test2::MojoX; +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 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. -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 = Test::Mojo->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_deeply($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' => 0); -$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' => 0); -$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[] => {