Source code for timApp.tests.server.test_jsrunner

"""Server tests for jsrunner plugin."""
import json
from datetime import datetime, timezone

import requests

from timApp.answer.answer import Answer
from timApp.auth.accesstype import AccessType
from timApp.document.docinfo import DocInfo
from timApp.document.viewcontext import default_view_ctx
from timApp.plugin.plugin import Plugin
from timApp.tests.server.timroutetest import TimRouteTest
from timApp.timdb.sqa import db
from timApp.user.usergroup import UserGroup
from timApp.user.usergroupmember import UserGroupMember


[docs]class JsRunnerTestBase(TimRouteTest):
[docs] def create_jsrun(self, md): return self.create_doc( initial_par=rf""" #- {{#r plugin=jsrunner}} {md}""" )
[docs] def do_jsrun( self, d: DocInfo, expect_content=None, expect_status=200, user_input=None, runner_name="r", **kwargs, ): if not user_input: user_input = {} return self.post_answer( "jsrunner", f"{d.id}.{runner_name}", user_input=user_input, expect_content=expect_content, expect_status=expect_status, **kwargs, )
[docs] def create_group_jsrun(self, grouplist, group="tg1", method="setGroup"): d = self.create_jsrun( f""" fields: [] group: testuser1 program: '' postprogram: |!! tools.{method}("{group}", {grouplist}); !!""" ) return d
[docs]class JsRunnerTest(JsRunnerTestBase):
[docs] def setUp(self): super().setUp() self.login_test1()
[docs] def test_invalid_markup(self): invalid_yamls = [ ("", "{'fields': ['Missing data for required field.']}", 400), ( "fields: []", "{'_schema': ['Either group or groups must be given.']}", 400, ), ( "fields: []\ngroup: 0", "{'group': [['Not a valid string.'], {'_schema': ['Invalid input type.']}]}", 400, ), ( "fields: []\ngroup: 1", "{'group': [['Not a valid string.'], {'_schema': ['Invalid input type.']}]}", 400, ), ( "fields: []\nprogram: 1", "{'program': [['Not a valid string.'], {'_schema': ['Invalid input type.']}]}", 400, ), ("fields: []\ngroup: xxx", "The following groups were not found: xxx", 400), ( "fields: []\ngroups: [xxx, yyy]", "The following groups were not found: xxx, yyy", 400, ), ("fields: []\ngroup: testuser1", "Attribute 'program' is required.", 400), # ('fields: [x.y]\ngroup: testuser1\nprogram: ""', # 'Invalid field access: y', 400), ( 'fields: []\ngroup: testuser1\nprogram: ""\ntimeout: 4500', { "web": { "error": "Invalid inputs to jsrunner answer route", "invalidInputs": [ { "actual": 4500, "key": "timeout", "type": "(number | <function1>)", } ], } }, 200, ), ( 'fields: []\ngroup: testuser1\nprogram: ""', {"web": {"output": "", "errors": [], "outdata": {}}}, 200, ), ] for y, e, s in invalid_yamls: d = self.create_jsrun(y) self.do_jsrun( d, expect_content=e, expect_status=s, )
# def test_nonexistent_field(self): # TODO: Check if obsolete # d = self.create_jsrun(""" # fields: # - y # program: |!! # tools.setDouble("x", 2.0) # !! # group: testuser1 # """) # self.do_jsrun( # d, # expect_content={'web': { # 'error': 'Task not found: x', # 'output': '', # 'errors': [], # }}, # )
[docs] def test_nonexistent_doc(self): d = self.create_jsrun( """ fields: - 999.x program: |!! tools.setDouble("x", 2.0) !! group: testuser1 """ ) self.do_jsrun( d, expect_content={"error": "Document 999 not found"}, expect_status=404, )
[docs] def test_infinite_loop(self): d = self.create_jsrun( """ fields: [] group: testuser1 timeout: 100 program: |!! while (true) {} !! """ ) self.do_jsrun( d, expect_content={ "web": { "fatalError": {"msg": "Script execution timed out."}, "output": "", } }, )
[docs] def test_syntax_error(self): d = self.create_jsrun( """ fields: [] group: testuser1 program: |!! { !! """ ) r = self.do_jsrun( d, ) self.assertEqual("Unexpected end of input", r["web"]["fatalError"]["msg"]) self.assertTrue( r["web"]["fatalError"]["stackTrace"].startswith("Unexpected token (1:1)\n") )
[docs] def test_invalid_field_name(self): d = self.create_jsrun( """ fields: [] group: testuser1 program: |!! tools.setString("1", "a"); !! """ ) self.do_jsrun( d, expect_content={"error": "Invalid task name: 1"}, expect_status=400, )
[docs] def test_no_multiple_saves(self): d = self.create_jsrun( """ fields: [] group: testuser1 program: |!! tools.setString("t1", "a"); !! """ ) d.document.add_text( """ #- {plugin=textfield #t1}""" ) self.do_jsrun( d, ) self.do_jsrun( d, ) self.verify_answer_content( f"{d.id}.t1", "c", "a", self.test_user_1, expected_count=1 )
[docs] def test_print_nonascii(self): d = self.create_jsrun( """ fields: [] group: testuser1 program: |!! tools.print("häh höh håh"); !! """ ) self.do_jsrun( d, expect_content={ "web": {"errors": [], "output": "häh höh håh\n", "outdata": {}} }, )
[docs] def test_aliases_and_getters(self): d1 = self.create_doc( initial_par=""" #- {plugin=textfield #t1} #- {plugin=textfield #t2} """ ) d2 = self.create_doc( initial_par=""" #- {plugin=textfield #t1} #- {plugin=textfield #t2}""" ) d = self.create_jsrun("") p = d.document.get_paragraphs()[0] p.set_markdown( f""" fields: - {d1.id}.t1=a1 - {d1.id}.t2=a2 - {d2.id}.t1=a3 - {d2.id}.t2=a4 - {d.id}.t1=a5 - t2=a6 group: testuser1 program: |!! tools.setString("a1", "al1"); tools.setString("a2", "al2"); tools.setString("a3", "al3"); tools.setString("a4", "al4"); tools.setDouble("a5", tools.getDouble("a5", 0) + 1); tools.setString("a6", "al6"); tools.setString("t2", "noalias"); tools.setString("{d2.id}.t2", "noalias2"); !! """ ) p.save() d.document.add_text( """ #- {plugin=textfield #t1} #- {plugin=textfield #t2}""" ) self.do_jsrun( d, ) self.verify_answer_content(f"{d1.id}.t1", "c", "al1", self.test_user_1) self.verify_answer_content(f"{d1.id}.t2", "c", "al2", self.test_user_1) self.verify_answer_content(f"{d2.id}.t1", "c", "al3", self.test_user_1) self.verify_answer_content(f"{d2.id}.t2", "c", "noalias2", self.test_user_1) self.verify_answer_content(f"{d.id}.t1", "c", 1, self.test_user_1) self.verify_answer_content(f"{d.id}.t2", "c", "noalias", self.test_user_1) self.do_jsrun( d, ) self.verify_answer_content( f"{d.id}.t1", "c", 2, self.test_user_1, expected_count=2 )
[docs] def test_invalid_aliases(self): invalid_yamls = [ ("- a=", "Alias cannot be empty: a=", 400), ("- a=b\n- c=b", "Duplicate alias b in fields attribute", 400), ("- a==", "Invalid alias: a==", 400), ] for y, e, s in invalid_yamls: d = self.create_jsrun( f""" fields: {y} group: testuser1 program: '' """ ) self.do_jsrun( d, expect_content=e, expect_status=s, )
[docs] def test_multiple_documents(self): d1 = self.create_doc( initial_par=""" #- {plugin=textfield #t1} #- {plugin=textfield #t2} """ ) d2 = self.create_doc( initial_par=""" #- {plugin=textfield #t1} #- {plugin=textfield #t2}""" ) d = self.create_jsrun("") p = d.document.get_paragraphs()[0] p.set_markdown( f""" fields: [] group: testuser1 program: |!! tools.setString("{d1.id}.t1", "a"); tools.setString("{d1.id}.t2", "b"); tools.setString("{d2.id}.t1", "c"); tools.setString("{d2.id}.t2", "d"); tools.setString("{d.id}.t1", "e"); tools.setString("t2", "f"); !!""" ) p.save() d.document.add_text( """ #- {plugin=textfield #t1} #- {plugin=textfield #t2} """ ) self.do_jsrun( d, ) self.verify_answer_content(f"{d1.id}.t1", "c", "a", self.test_user_1) self.verify_answer_content(f"{d1.id}.t2", "c", "b", self.test_user_1) self.verify_answer_content(f"{d2.id}.t1", "c", "c", self.test_user_1) self.verify_answer_content(f"{d2.id}.t2", "c", "d", self.test_user_1) self.verify_answer_content(f"{d.id}.t1", "c", "e", self.test_user_1) self.verify_answer_content(f"{d.id}.t2", "c", "f", self.test_user_1)
[docs] def test_setters_and_getters(self): d = self.create_jsrun( """ fields: [] program: |!! tools.setDouble("t01", 2) tools.setDouble("t02", 2.1) tools.setDouble("t03", "2") tools.setDouble("t04", "2.1") //tools.setDouble("t05", null) //tools.setDouble("t06", undefined) //tools.setDouble("t07", {}) tools. setInt("t08", 2) //tools. setInt("t09", 2.4) //tools. setInt("t10", 2.5) //tools. setInt("t11", 2.6) //tools. setInt("t12", "2.4") tools. setInt("t13", "2") //tools. setInt("t14", null) //tools. setInt("t15", undefined) //tools. setInt("t16", {}) tools.setString("t17", "") tools.setString("t18", "a") tools.setString("t19", "1") tools.setString("t20", 1) tools.setString("t21", 1.6) //tools.setString("t22", null) //tools.setString("t23", undefined) //tools.setString("t24", {}) !! group: testuser1 """ ) d.document.add_text( """ #- {defaultplugin=textfield} {% for i in range(1, 25) %} {#t%%'%02d'|format(i)%%#} {% endfor %} """ ) self.do_jsrun( d, expect_content={ "web": { "output": "", "errors": [], "outdata": {}, } }, ) self.verify_answer_content(f"{d.id}.t01", "c", 2, self.test_user_1) self.verify_answer_content(f"{d.id}.t02", "c", 2.1, self.test_user_1) self.verify_answer_content(f"{d.id}.t03", "c", 2, self.test_user_1) self.verify_answer_content(f"{d.id}.t04", "c", 2.1, self.test_user_1) self.verify_answer_content(f"{d.id}.t08", "c", 2, self.test_user_1) self.verify_answer_content(f"{d.id}.t13", "c", 2, self.test_user_1) self.verify_answer_content(f"{d.id}.t17", "c", "", self.test_user_1) self.verify_answer_content(f"{d.id}.t18", "c", "a", self.test_user_1) self.verify_answer_content(f"{d.id}.t19", "c", "1", self.test_user_1) self.verify_answer_content(f"{d.id}.t20", "c", "1", self.test_user_1) self.verify_answer_content(f"{d.id}.t21", "c", "1.6", self.test_user_1)
[docs] def test_rights(self): d = self.create_doc( initial_par=""" #- {plugin=textfield #t} """ ) self.test_user_2.grant_access(d, AccessType.view) db.session.commit() self.login_test2() d2 = self.create_jsrun( f""" group: testuser2 fields: - {d.id}.t program: |!! tools.setString("{d.id}.t", "hi"); !! """ ) self.do_jsrun( d2, expect_content=f"Missing teacher access for document {d.id}", expect_status=403, ) d2 = self.create_jsrun( f""" group: testuser2 fields: [] program: |!! tools.setString("{d.id}.t", "hi"); !! """ ) self.do_jsrun( d2, expect_content=f"Missing teacher access for document {d.id}", expect_status=403, ) # Can write own answer to another doc via jsrunner if teacher access there self.test_user_2.grant_access(d, AccessType.teacher) db.session.commit() self.do_jsrun( d2, ) a = self.verify_answer_content(f"{d.id}.t", "c", "hi", self.test_user_2) self.test_user_2.remove_access(d.id, "teacher") db.session.commit() # Can write own answer to another doc via jsrunner if view access and allow_ext d.document.add_setting("allow_external_jsrunner", True) self.test_user_2.remove_access(d.id, "view") db.session.commit() d2 = self.create_jsrun( f""" group: testuser2 fields: [] program: |!! tools.setString("{d.id}.t", "hi_ext"); !! """ ) self.do_jsrun( d2, expect_content=f"Missing teacher access for document {d.id}", expect_status=403, ) self.test_user_2.grant_access(d, AccessType.view) db.session.commit() self.do_jsrun( d2, ) a = self.verify_answer_content( f"{d.id}.t", "c", "hi_ext", self.test_user_2, expected_count=2 ) d2 = self.create_jsrun( f""" group: testuser1 fields: [] program: |!! tools.setString("t", "hi"); !! """ ) d2.document.add_text("#- {#t plugin=textfield}") self.do_jsrun( d2, ) a = self.verify_answer_content(f"{d2.id}.t", "c", "hi", self.test_user_2) self.test_user_2.groups.append(UserGroup.get_teachers_group()) db.session.commit() self.do_jsrun( d2, ) a = self.verify_answer_content( f"{d2.id}.t", "c", "hi", self.test_user_1, expected_count=1 ) self.assertEqual(self.test_user_2, a.saver) # Can use jsrunner for own group in another doc if view access and showInView: true self.login_test1() d2 = self.create_jsrun( f""" groups: [] fields: - t program: |!! tools.setString("t", "hi"); !! """ ) d2.document.add_text("#- {#t plugin=textfield}") self.test_user_2.grant_access(d2, AccessType.view) db.session.commit() d3 = self.create_jsrun( f""" groups: [] fields: - t program: |!! tools.setString("t", "hi"); !! showInView: true """ ) d3.document.add_text("#- {#t plugin=textfield}") self.test_user_2.grant_access(d3, AccessType.view) db.session.commit() self.login_test2() self.do_jsrun( d2, expect_content=f"Missing teacher access for document {d2.id}", expect_status=403, ) self.do_jsrun( d3, ) a = self.verify_answer_content(f"{d3.id}.t", "c", "hi", self.test_user_2)
[docs] def test_runscript(self): runscript_url = "http://jsrunner:5000/runScript" r = requests.post(runscript_url) self.assertEqual(400, r.status_code) self.assertEqual( {"error": "Invalid input to jsrunner runScript route."}, r.json() ) r = requests.post( runscript_url, json={ "code": """ const result = {x: data.a + data.b, y: data.a - data.b}; return result; """, "data": {"a": 7, "b": 11}, }, ) self.assertEqual(200, r.status_code) self.assertEqual({"result": {"x": 18, "y": -4}, "output": ""}, r.json()) r = requests.post( runscript_url, json={"timeout": 10, "code": """while(true){}""", "data": {}} ) self.assertEqual(200, r.status_code) self.assertEqual({"error": "Script execution timed out."}, r.json()) r = requests.post( runscript_url, json={"timeout": 10000, "code": """while(true){}""", "data": {}}, ) self.assertEqual(400, r.status_code) self.assertEqual( {"error": "Invalid input to jsrunner runScript route."}, r.json() ) r = requests.post(runscript_url, json={"code": """{""", "data": {}}) self.assertEqual(200, r.status_code) self.assertEqual( { "error": "Unexpected end of input [script.js:18:28]\n<pre>\n11: {\n</pre>\n" }, r.json(), ) r = requests.post( runscript_url, json={ "code": """ class X {} return X; """, "data": {}, }, ) self.assertEqual(200, r.status_code) self.assertEqual( { "error": "Script failed to return anything (the return value must be JSON serializable)." }, r.json(), )
[docs] def test_tally_fields(self): fd = self.create_doc(initial_par="""#- {#t plugin=textfield}""") self.current_user.answers.append( Answer( task_id=f"{fd.id}.t", points=2.5, content=json.dumps({"c": "x"}), valid=True, ) ) db.session.commit() d = self.create_jsrun( f""" fields: - tally:total_points[2018-04-06 12:00:00, 2019-06-05 12:00:00]=total_range - tally:total_points[,2019-06-05 12:00:00]=total_range_end - tally:total_points[2018-04-06 12:00:00,]=total_range_start - tally:total_points=total_all - tally:total_points[,]=total_all_alt - tally:1st=a - tally:2nd=b - tally:3rd=c - tally:task_count - tally:{fd.id}.total_points=otherdoc group: testuser1 program: |!! tools.print(tools.getDouble("a")); tools.print(tools.getDouble("b")); tools.print(tools.getDouble("c")); tools.print(tools.getDouble("total_all")); tools.print(tools.getDouble("total_range")); tools.print(tools.getDouble("task_count")); tools.print(tools.getDouble("otherdoc")); !! """ ) d.document.set_settings( { "point_sum_rule": { "groups": { "1st": "a.*", "2nd": "b.*", "3rd": "c.*", }, "count": {"best": 2}, } }, ) d.document.add_text( """ #- {defaultplugin=textfield} {#a01#} {#a02#} {#a03#} {#b01#} {#b02#} {#b03#} {#c01#} {#c02#} {#c03#} """ ) u = self.current_user for i in range(1, 4): for letter, num in zip(["a", "b", "c"], [1, 2, 3]): c = 10 ** (i - 1) * num tid = f"{d.id}.{letter}0{i}" a = Answer( task_id=tid, content=json.dumps({"c": c}), points=c, valid=True, ) if i == 2 and letter == "c": a.answered_on = datetime(2018, 6, 20) u.answers.append(a) db.session.commit() self.do_jsrun( d, expect_content={ "web": { "errors": [], "outdata": {}, "output": "111\n222\n333\n555\n30\n3\n2.5\n", } }, )
[docs] def test_mix_tally_and_normal_fields(self): c = json.dumps({"c": ""}) f1 = self.create_doc(initial_par="#- {#t1 plugin=textfield}") f2 = self.create_doc(initial_par="#- {#t3 plugin=textfield}") curr = self.current_user curr.answers.append( Answer(task_id=f"{f1.id}.t1", points=1, valid=True, content=c) ) curr.answers.append( Answer(task_id=f"{f2.id}.t3", points=10, valid=True, content=c) ) db.session.commit() d = self.create_jsrun( f""" groups: - testuser1 fields: - tally:{f1.id}.total_points=t11 - {f2.id}.t3.points=d11 - op(0,5) - t1 - op1.count - d1 updateFields: - t1 - d1 program: |!! tools.print( tools.getDouble("t1"), tools.getDouble("d1"), ); tools.setDouble("t1", tools.getSum("t1", 1, 1), 0); tools.setDouble("d1", tools.getSum("d1", 1, 1), 0); tools.print( tools.getDouble("t1"), tools.getDouble("d1"), tools.getDouble("op0"), tools.getDouble("op1"), tools.getDouble("op2"), tools.getDouble("op3"), tools.getDouble("op4"), tools.getValue("op1.count"), ); !!""" ) d.document.add_text( """ #- {defaultplugin=textfield} {#op0#} {#op1#} {#op2#} {#op3#} {#op4#} {#t1#} {#d1#} """ ) for i in range(0, 5): self.current_user.answers.append( Answer( task_id=f"{d.id}.op{i}", valid=True, content=json.dumps({"c": 10 ** (i + 2)}), ) ) db.session.commit() self.do_jsrun( d, expect_content={ "web": { "errors": [], "outdata": {}, "output": "0 0\n1 10 100 1000 10000 100000 1000000 1\n", }, }, )
[docs] def test_no_points_answers_tally(self): d = self.create_jsrun( """ fields: - tally:total_points=x group: testuser1 program: |!! tools.print(tools.getDouble("x")); !! """ ) d.document.add_text("""#- {#t plugin=textfield}""") # Add an answer with no points. self.current_user.answers.append( Answer(task_id=f"{d.id}.t", valid=True, content=json.dumps({"c": ""})) ) db.session.commit() self.do_jsrun( d, expect_content={"web": {"errors": [], "outdata": {}, "output": "0\n"}}, )
[docs] def test_invalid_tally_fields(self): invalid_yamls = [ ( "tally:total_points[2018-04-06 12:00:00, 2019-06-05 12:00:00=total_range", "Invalid tally field format: tally:total_points[2018-04-06 12:00:00, 2019-06-05 12:00:00", 400, ), ( "tally:total_points[2018-04-06 12:00:00]=total_range", "Invalid tally field format: tally:total_points[2018-04-06 12:00:00]", 400, ), ( "tally:total_points[2018-04-06 12:00:00, 2019-06-05 12:00:60]=total_range", "Invalid tally field format: tally:total_points[2018-04-06 12:00:00, 2019-06-05 12:00:60]", 400, ), ( "tally:x", "Unknown tally field: x. Valid tally fields are: total_points, velp_points, task_points, task_count and velped_task_count.", 400, ), ] d = None for y, e, s in invalid_yamls: d = self.create_jsrun( f""" fields: - {y} group: testuser1 program: |!! !!""" ) self.do_jsrun( d, expect_content=e, expect_status=s, ) d.document.set_settings( { "point_sum_rule": { "groups": { "1st": "a.*", "2nd": "b.*", "3rd": "c.*", }, } } ) self.do_jsrun( d, expect_content="Unknown tally field: x. Valid tally fields are: total_points, velp_points, task_points, task_count, velped_task_count, 1st, 2nd and 3rd.", expect_status=400, )
[docs] def test_deleted_users(self): d = self.create_jsrun( """ fields: [] group: testusers includeUsers: all selectIncludeUsers: true program: |!! tools.print(tools.getLeaveDate()); !!""" ) ug = UserGroup.create("testusers") ug.current_memberships[self.test_user_1.id] = UserGroupMember( user=self.test_user_1 ) ug.current_memberships[self.test_user_2.id] = UserGroupMember( user=self.test_user_2, membership_end=datetime(2010, 1, 1).replace(tzinfo=timezone.utc), ) db.session.commit() self.do_jsrun( d, user_input={"includeUsers": "x"}, expect_status=400, expect_content={ "error": "{'input': {'includeUsers': [['Invalid enum value x'], {'_schema': " "['Invalid input type.']}]}}" }, ) self.do_jsrun( d, user_input={"includeUsers": "current"}, expect_content={"web": {"output": "null\n", "errors": [], "outdata": {}}}, ) self.do_jsrun( d, user_input={"includeUsers": "all"}, expect_content={"web": {"output": "null\n", "errors": [], "outdata": {}}}, ) gd = self.create_doc() UserGroup.get_by_name("testusers").admin_doc = gd.block db.session.commit() self.do_jsrun( d, user_input={"includeUsers": "all"}, expect_content={ "web": { "errors": [], "outdata": {}, "output": "null\n1262304000\n", } }, ) p = d.document.get_paragraphs()[0] plug = Plugin.from_paragraph(p, default_view_ctx) plug.values["selectIncludeUsers"] = False plug.save() self.do_jsrun( d, user_input={"includeUsers": "current"}, expect_status=403, expect_content="Not allowed to select includeUsers option.", ) self.do_jsrun( d, user_input={"includeUsers": "all"}, expect_content={ "web": { "errors": [], "outdata": {}, "output": "null\n1262304000\n", } }, ) plug.values.pop("selectIncludeUsers") plug.save() self.do_jsrun( d, user_input={"includeUsers": "current"}, expect_status=403, expect_content="Not allowed to select includeUsers option.", ) self.do_jsrun( d, user_input={"includeUsers": "all"}, expect_content={ "web": { "errors": [], "outdata": {}, "output": "null\n1262304000\n", } }, )
[docs] def test_add_time_log(self): d = self.create_jsrun( """ fields: [] group: testusers_addtest includeUsers: all selectIncludeUsers: true program: |!! tools.print(tools.getAddDate()); !!""" ) ug = UserGroup.create("testusers_addtest") ug.current_memberships[self.test_user_1.id] = UserGroupMember( user=self.test_user_1, membership_added=datetime(2010, 1, 1).replace(tzinfo=timezone.utc), ) ug.current_memberships[self.test_user_2.id] = UserGroupMember( user=self.test_user_2, membership_added=datetime(2020, 1, 1).replace(tzinfo=timezone.utc), membership_end=datetime(2020, 2, 1).replace(tzinfo=timezone.utc), ) gd = self.create_doc() UserGroup.get_by_name("testusers_addtest").admin_doc = gd.block db.session.commit() self.do_jsrun( d, user_input={"includeUsers": "current"}, expect_content={ "web": { "errors": [], "outdata": {}, "output": "1262304000\n", } }, ) self.do_jsrun( d, user_input={"includeUsers": "all"}, expect_content={ "web": { "errors": [], "outdata": {}, "output": "1262304000\n1577836800\n", } }, )
[docs] def test_collaborators(self): d = self.create_jsrun( """ fields: - a group: collabs program: |!! tools.print(tools.getRealName()); tools.print(tools.getString("a")); !!""" ) d.document.add_text( """ #- {plugin=textfield #a} """ ) self.create_test_group("collabs") a = Answer(content=json.dumps({"c": "test"}), valid=True, task_id=f"{d.id}.a") self.test_user_1.answers.append(a) self.test_user_2.answers.append(a) db.session.commit() self.do_jsrun( d, expect_content={ "web": { "errors": [], "outdata": {}, "output": "Test user 1\ntest\nTest user 2\ntest\n", }, }, )
[docs] def create_test_group(self, ugname: str): ug = UserGroup.create(ugname) ug.users.append(self.test_user_1) ug.users.append(self.test_user_2) ug.admin_doc = self.create_doc().block return ug
[docs] def test_global_field(self): d = self.create_jsrun( """ fields: - GLO_a group: tg2 program: |!! tools.setString("GLO_a", tools.getString("GLO_a") + "b") !! """ ) d.document.add_text( """ #- {plugin=textfield #GLO_a} """ ) self.create_test_group("tg2") a = Answer(content=json.dumps({"c": "a"}), valid=True, task_id=f"{d.id}.GLO_a") self.test_user_1.answers.append(a) db.session.commit() total_answer_count_before = Answer.query.count() self.do_jsrun(d) total_answer_count_after = Answer.query.count() self.assertEqual(1, total_answer_count_after - total_answer_count_before) self.verify_answer_content( f"{d.id}.GLO_a", "c", "ab", self.test_user_1, expected_count=2 ) self.verify_answer_content( f"{d.id}.GLO_a", "c", "ab", self.test_user_2, expected_count=0 )
[docs] def test_styles(self): d = self.create_jsrun( """ fields: [] group: testuser1 program: |!! tools.setString("t.styles", JSON.stringify({backgroundColor: "red"})); !! """ ) d.document.add_text( """ #- {plugin=textfield #t} #- {plugin=jsrunner #r2} fields: [] group: testuser1 program: |!! tools.setString("t", "x"); !! #- {plugin=jsrunner #r3} fields: [] group: testuser1 program: |!! tools.setDouble("t", 0); !! #- {plugin=jsrunner #r4} fields: [] group: testuser1 program: |!! tools.setString("t.styles", JSON.stringify({backgroundColor: "green"})); !! #- {plugin=jsrunner #r5} fields: [] group: testuser1 program: |!! tools.setString("t.styles", ""); !! """ ) self.do_jsrun(d) self.verify_answer_content( f"{d.id}.t", None, {"styles": {"backgroundColor": "red"}, "c": None}, self.test_user_1, ) self.do_jsrun(d) self.verify_answer_content( f"{d.id}.t", None, {"styles": {"backgroundColor": "red"}, "c": None}, self.test_user_1, ) self.do_jsrun(d, runner_name="r2") self.verify_answer_content( f"{d.id}.t", None, {"styles": {"backgroundColor": "red"}, "c": "x"}, self.test_user_1, expected_count=2, ) self.do_jsrun(d) self.verify_answer_content( f"{d.id}.t", None, {"styles": {"backgroundColor": "red"}, "c": "x"}, self.test_user_1, expected_count=2, ) self.do_jsrun(d, runner_name="r3") self.verify_answer_content( f"{d.id}.t", None, {"styles": {"backgroundColor": "red"}, "c": 0}, self.test_user_1, expected_count=3, ) self.do_jsrun(d) self.verify_answer_content( f"{d.id}.t", None, {"styles": {"backgroundColor": "red"}, "c": 0}, self.test_user_1, expected_count=3, ) self.do_jsrun(d, runner_name="r4") self.verify_answer_content( f"{d.id}.t", None, {"styles": {"backgroundColor": "green"}, "c": 0}, self.test_user_1, expected_count=4, ) self.do_jsrun(d, runner_name="r5") self.verify_answer_content( f"{d.id}.t", None, {"c": 0}, self.test_user_1, expected_count=5 ) self.do_jsrun(d, runner_name="r5") self.verify_answer_content( f"{d.id}.t", None, {"c": 0}, self.test_user_1, expected_count=5 )
[docs] def test_points(self): d = self.create_jsrun( """ fields: [] group: testuser1 program: |!! tools.setDouble("t.points", 1); !! """ ) d.document.add_text("#- {plugin=textfield #t}") self.do_jsrun(d) a = self.verify_answer_content(f"{d.id}.t", None, {"c": None}, self.test_user_1) self.assertEqual(1, a.points) self.do_jsrun(d) self.verify_answer_content(f"{d.id}.t", None, {"c": None}, self.test_user_1)
[docs] def test_group_wildcard(self): self.login_test1() def run_js(doc, output): self.do_jsrun( doc, expect_content={ "web": { "errors": [], "outdata": {}, "output": output, }, }, ) def make_doc(*groups): d = self.create_jsrun( f""" fields: [mmcqt] groups: [{', '.join(groups)}] program: |!! tools.println(tools.getRealName()); !! """ ) d.document.add_text( """ #- {plugin=mmcq #mmcqt} stem: "foo" choices: - correct: true text: "a" - correct: false text: "b" """ ) return d d1 = make_doc('"*"') self.add_answer(d1, "mmcqt", content=[True, False], user=self.test_user_1) self.add_answer(d1, "mmcqt", content=[True, True], user=self.test_user_2) db.session.commit() run_js(d1, "Test user 1\nTest user 2\n") ug = self.create_test_group("jsrunnertestgroup1") self.test_user_3.add_to_group(ug, None) d2 = make_doc('"*"', "jsrunnertestgroup1") self.add_answer(d2, "mmcqt", content=[True, False], user=self.test_user_1) self.add_answer(d2, "mmcqt", content=[True, True], user=self.test_user_2) db.session.commit() # Test User 3 appears in the result along with all the answered users expected_names = "Test user 1\nTest user 2\nTest user 3\n" run_js(d2, expected_names) self.add_answer(d2, "mmcqt", content=[True, True], user=self.test_user_3) db.session.commit() # Test User 3 is not duplicated in the list run_js(d2, expected_names) # Test that current user is not visible if he hasn't answered d3 = make_doc('"*"') run_js(d3, "")
[docs] def test_mcq_qst_fetch(self): d = self.create_jsrun( """ fields: [t, t2] group: testuser1 program: |!! const t = tools.getValue("t"); tools.println(t); const t2 = tools.getValue("t2"); tools.println(t2); !! """ ) d.document.add_text( """ #- {plugin=mcq #t} headerText: '' stem: "" choices: - text: "first" - text: "second" ``` {#t2 plugin="qst"} answerFieldType: radio answerLimit: 1 expl: {} headers: [] questionText: x questionTitle: x questionType: radio-vertical rows: - a - b ``` """ ) self.add_answer(d, "t", 1, content_key=None, user=self.test_user_1) self.add_answer(d, "t2", [["2"]], content_key=None, user=self.test_user_1) db.session.commit() self.do_jsrun( d, expect_content={ "web": {"errors": [], "outdata": {}, "output": '1\n[["2"]]\n'} }, )
[docs] def test_count_field(self): d = self.create_jsrun( """ fields: - t1.count - t2.count group: tg program: |!! tools.println(tools.getDouble("t1.count", 0)); tools.println(tools.getValue("t2.count")); !! """ ) self.create_test_group("tg") d.document.add_text("#- {plugin=textfield #t1}\n#- {plugin=textfield #t2}") self.add_answer(d, "t1", "tu1.1.1", user=self.test_user_1) self.add_answer(d, "t1", "tu1.1.2", user=self.test_user_1) self.add_answer(d, "t1", "tu1.1.3", user=self.test_user_1) self.add_answer(d, "t2", "tu2.2.1", user=self.test_user_2) db.session.commit() self.do_jsrun( d, expect_content={ "web": {"errors": [], "outdata": {}, "output": "3\n0\n0\n1\n"} }, )
[docs]class JsRunnerGroupTest(JsRunnerTestBase):
[docs] def test_jsrunner_group(self): self.login_test1() d = self.create_jsrun( """ fields: [t1, t2] group: testusers program: |!! tools.setString("t1", tools.getString("t1", "") + "-" + tools.getRealName()); tools.setString("t2", tools.getString("t2", "") + "=" + tools.getRealName()); !! """ ) d.document.add_text( """ #- {#t1 plugin=textfield#} #- {#t2 plugin=textfield#} """ ) ug = UserGroup.create("testusers") ug.users.append(self.test_user_1) ug.users.append(self.test_user_2) ug.users.append(self.test_user_3) self.test_user_1.groups.append(UserGroup.get_teachers_group()) db.session.commit() self.do_jsrun( d, expect_content={"web": {"errors": [], "output": "", "outdata": {}}}, ) self.verify_answer_content(f"{d.id}.t1", "c", "-Test user 1", self.test_user_1) self.verify_answer_content(f"{d.id}.t2", "c", "=Test user 1", self.test_user_1) self.verify_answer_content(f"{d.id}.t1", "c", "-Test user 2", self.test_user_2) self.verify_answer_content(f"{d.id}.t2", "c", "=Test user 2", self.test_user_2) self.do_jsrun( d, expect_content={"web": {"errors": [], "output": "", "outdata": {}}}, ) self.verify_answer_content( f"{d.id}.t1", "c", "-Test user 1-Test user 1", self.test_user_1, expected_count=2, ) self.verify_answer_content( f"{d.id}.t2", "c", "=Test user 1=Test user 1", self.test_user_1, expected_count=2, ) self.verify_answer_content( f"{d.id}.t1", "c", "-Test user 2-Test user 2", self.test_user_2, expected_count=2, ) self.verify_answer_content( f"{d.id}.t2", "c", "=Test user 2=Test user 2", self.test_user_2, expected_count=2, ) self.do_jsrun( d, expect_content={"web": {"errors": [], "output": "", "outdata": {}}}, ) self.verify_answer_content( f"{d.id}.t1", "c", "-Test user 1-Test user 1-Test user 1", self.test_user_1, expected_count=3, ) self.verify_answer_content( f"{d.id}.t2", "c", "=Test user 1=Test user 1=Test user 1", self.test_user_1, expected_count=3, ) self.verify_answer_content( f"{d.id}.t1", "c", "-Test user 2-Test user 2-Test user 2", self.test_user_2, expected_count=3, ) self.verify_answer_content( f"{d.id}.t2", "c", "=Test user 2=Test user 2=Test user 2", self.test_user_2, expected_count=3, )
[docs] def test_group_create(self): self.login_test1() d = self.create_jsrun( """ fields: [] group: testuser1 program: |!! tools.setGroup("tg1", []); !!""" ) # Group methods are only usable in postProgram. self.do_jsrun( d, expect_content={ "web": { "fatalError": { "msg": "tools.setGroup is not a function", "stackTrace": "Index (1:24)\n" "program:\n" '01: tools.setGroup("tg1", []);\n', }, "output": "", } }, ) d_empty = self.create_group_jsrun([]) self.do_jsrun( d_empty, expect_content="Creating group tg1: This action requires group administrator rights.", expect_status=403, ) ug = UserGroup.get_groupadmin_group() self.test_user_1.add_to_group(ug, None) db.session.commit() self.do_jsrun( d_empty, expect_content={"web": {"errors": [], "outdata": {}, "output": ""}}, ) self.assertIsNotNone(UserGroup.get_by_name("tg1")) d = self.create_group_jsrun([self.test_user_1.id, 999]) self.do_jsrun( d, expect_content="Users not found: {999}", expect_status=400, ) d = self.create_group_jsrun([self.test_user_1.id, self.test_user_2.id]) self.do_jsrun( d, expect_content={"web": {"output": "", "errors": [], "outdata": {}}}, ) def expect_tg1_members(members): ug = UserGroup.get_by_name("tg1") self.assertEqual(members, set(ug.users)) expect_tg1_members({self.test_user_1, self.test_user_2}) self.do_jsrun( d_empty, expect_content={"web": {"output": "", "errors": [], "outdata": {}}}, ) expect_tg1_members(set()) d1 = self.create_group_jsrun([self.test_user_1.id], method="addToGroup") d2 = self.create_group_jsrun([self.test_user_2.id], method="addToGroup") self.do_jsrun( d1, expect_content={"web": {"output": "", "errors": [], "outdata": {}}}, ) self.do_jsrun( d2, expect_content={"web": {"output": "", "errors": [], "outdata": {}}}, ) expect_tg1_members({self.test_user_1, self.test_user_2}) d1 = self.create_group_jsrun([self.test_user_1.id], method="removeFromGroup") d2 = self.create_group_jsrun([self.test_user_2.id], method="removeFromGroup") self.do_jsrun( d1, expect_content={"web": {"output": "", "errors": [], "outdata": {}}}, ) expect_tg1_members({self.test_user_2}) self.do_jsrun( d2, expect_content={"web": {"output": "", "errors": [], "outdata": {}}}, ) expect_tg1_members(set()) d = self.create_jsrun( """ fields: [] group: testuser1 program: '' postprogram: |!! for (let i = 0; i < 11; ++i) { tools.setGroup("tgx" + i, []); } !!""" ) self.do_jsrun( d, expect_content="Maximum of 10 groups can be created per one jsrunner run.", expect_status=400, )
[docs] def test_user_filtering(self): # Group teacher can filter jsrunner targets but not use runner on users outside of the group self.login_test1() d = self.create_jsrun( """ fields: [] group: testuser3isnothere program: |!! tools.print(tools.getUserName()); !!""" ) ug = UserGroup.create("testuser3isnothere") ug.users.append(self.test_user_1) ug.users.append(self.test_user_2) ug.admin_doc = self.create_doc().block self.test_user_2.grant_access(ug.admin_doc, AccessType.teacher) self.test_user_2.grant_access(d, AccessType.teacher) db.session.commit() self.login_test2() self.do_jsrun( d, user_input={"userNames": ["testuser1"]}, expect_content={ "web": {"errors": [], "output": "testuser1\n", "outdata": {}} }, ) self.do_jsrun( d, user_input={"userNames": ["testuser3"]}, expect_content={"web": {"errors": [], "output": "", "outdata": {}}}, )