smc-hub
Version:
CoCalc: Backend webserver component
420 lines (371 loc) • 18.7 kB
text/coffeescript
#########################################################################
# This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
# License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
#########################################################################
###
TESTING of user queries specifically involving changefeeds - part 1 -- accounts, file_use, project_log
COPYRIGHT : (c) 2017 SageMath, Inc.
LICENSE : AGPLv3
###
async = require('async')
expect = require('expect')
pgtest = require('./pgtest')
db = undefined
setup = (cb) -> (pgtest.setup (err) -> db=pgtest.db; cb(err))
teardown = pgtest.teardown
{create_accounts, create_projects, changefeed_series} = pgtest
misc = require('smc-util/misc')
describe 'test the accounts table changefeed', ->
@timeout(10000)
before(setup)
after(teardown)
account_id = undefined
changefeed_id = misc.uuid()
it 'writes to user accounts table and verify that change automatically appears in changefeed', (done) ->
async.series([
(cb) ->
db.create_account(first_name:"Sage", last_name:"Math", created_by:"1.2.3.4",\
email_address:"sage@example.com", password_hash:"blah", cb:\
(err, x) -> account_id=x; cb(err))
(cb) ->
db.user_query
account_id : account_id
query : {accounts:[{account_id:account_id, first_name:null}]}
changes : changefeed_id
cb : changefeed_series([
(x, cb) ->
db.user_query
account_id : account_id
query : {accounts:{account_id:account_id, first_name:'SAGE!'}}
cb : cb
(x, cb) ->
expect(x).toEqual({ action:'insert', new_val: { account_id:account_id, first_name:'SAGE!'}})
db.delete_account(account_id:account_id, cb:cb)
(x, cb) ->
expect(x).toEqual({ action: 'delete', old_val: { account_id:account_id } })
db.user_query_cancel_changefeed(id:changefeed_id, cb:cb)
], cb)
], (err) ->
if err
done(err)
else
done()
)
describe 'test changefeeds involving the file_use table on one project with one user', ->
@timeout(10000)
before(setup)
after(teardown)
accounts = []
projects = []
it 'create accounts and projects', (done) ->
async.series([
(cb) ->
create_accounts 1, (err, x) -> accounts=x; cb()
(cb) ->
create_projects 1, accounts[0], (err, x) -> projects.push(x...); cb(err)
], done)
it 'tests a changefeed on the file_use table for a single project', (done) ->
id = misc.uuid()
t0 = new Date()
t1 = t2 = undefined
obj = {project_id:projects[0], path: 'foo.txt', users:{"#{accounts[0]}":{'read':t0}, last_edited:t0}}
obj2 = {project_id:projects[0], path: 'foo2.txt', last_edited:new Date()}
db.user_query
account_id : accounts[0]
query : {file_use:[{id: null, project_id:projects[0], path: null, users: null, last_edited: null}]}
changes : id
cb : changefeed_series([
(x, cb) ->
expect(x).toEqual({ file_use: [] }) # how it starts
db.user_query
account_id : accounts[0]
query : {file_use:obj}
cb : cb
(x, cb) ->
obj.id = db.sha1(obj.project_id, obj.path)
expect(x).toEqual({action:'insert', new_val: obj })
# now mutate it by chatting on it
t1 = new Date()
db.user_query
account_id : accounts[0]
query : {file_use:{project_id:projects[0], path: 'foo.txt', users:{"#{accounts[0]}":{'chat':t1}}}}
cb : cb
(x, cb) ->
# note how chat gets recursively MERGED IN -- not replacing users. (so tricky under the hood to implement...)
obj.users["#{accounts[0]}"].chat = t1
expect(x.action).toEqual('update');
expect(x.new_val.users).toEqual(obj.users)
# now mutate it by updating last_edited
t2 = new Date()
db.user_query
account_id : accounts[0]
query : {file_use:{project_id:projects[0], path: 'foo.txt', last_edited:t2}}
cb : cb
(x, cb) ->
obj.last_edited = t2
expect(x.action).toEqual('update');
expect(x.new_val.last_edited).toEqual(obj.last_edited)
# add a second file_use entry
db.user_query
account_id : accounts[0]
query : {file_use:obj2}
cb : cb
(x, cb) ->
obj2.id = db.sha1(obj2.project_id, obj2.path)
expect(x).toEqual({action:'insert', new_val:obj2})
# now delete the first entry (not through file_use, but directly)
db._query
query : "DELETE FROM file_use"
where : {'id = $': obj.id}
cb : cb
(x, cb) ->
expect(x).toEqual({action:"delete", old_val:{id:obj.id, project_id:projects[0]}})
# and the second
db._query
query : "DELETE FROM file_use"
where : {'id = $': obj2.id}
cb : cb
(x, cb) ->
expect(x).toEqual({action:"delete", old_val:{id:obj2.id, project_id:projects[0]}})
db.user_query_cancel_changefeed(id:id, cb:cb)
], done)
describe 'test file_use changefeeds with multiple projects', ->
before(setup)
after(teardown)
accounts = []
projects = []
it 'create account and projects', (done) ->
async.series([
(cb) ->
create_accounts 2, (err, x) -> accounts=x; cb()
(cb) ->
create_projects 2, accounts[0], (err, x) -> projects.push(x...); cb(err)
(cb) ->
create_projects 1, accounts[1], (err, x) -> projects.push(x...); cb(err)
], done)
it 'insert into file_use for three separate projects', (done) ->
id = misc.uuid()
t = [misc.minutes_ago(10), misc.minutes_ago(5), misc.minutes_ago(3)]
obj = [
{project_id:projects[0], path: 'file-in-project0.txt', users:{"#{accounts[0]}":{'read':t[0]}}},
{project_id:projects[1], path: 'file-in-project1.txt', users:{"#{accounts[0]}":{'chat':t[1]}}},
{project_id:projects[2], path: 'file-in-project2.txt', users:{"#{accounts[1]}":{'chat':t[2]}}}
]
for x in obj
x.id = db.sha1(x.project_id, x.path)
db.user_query
account_id : accounts[0]
query : {file_use:[{id: null, project_id:null, path: null, users: null, last_edited: null}]}
changes : id
cb : changefeed_series([
(x, cb) ->
expect(x).toEqual({ file_use: [] }) # how it starts
db.user_query # insert first object
account_id : accounts[0]
query : {file_use:obj[0]}
cb : cb
(x, cb) ->
expect(x).toEqual({action:'insert', new_val: obj[0]})
db.user_query # insert second object
account_id : accounts[0]
query : {file_use:obj[1]}
cb : cb
(x, cb) ->
expect(x).toEqual({action:'insert', new_val: obj[1]})
db.user_query # insert third object, which should NOT trigger change
account_id : accounts[1]
query : {file_use:obj[2]}
cb : () =>
obj[1].last_edited = new Date()
db.user_query # insert second object again, modified
account_id : accounts[0]
query : {file_use:obj[1]}
cb : cb
(x, cb) ->
expect(x.action).toEqual('update');
expect(x.new_val.last_edited).toEqual(obj[1].last_edited)
cb()
], done)
describe 'modifying a single file_use record in various ways', ->
before(setup)
after(teardown)
accounts = []
projects = []
it 'create account and projects', (done) ->
async.series([
(cb) ->
# 2 accounts
create_accounts 2, (err, x) -> accounts=x; cb()
(cb) ->
# 1 project with both accounts using it
create_projects 1, accounts, (err, x) -> projects.push(x...); cb(err)
], done)
it 'insert a file_use entry and modify in various ways', (done) ->
changefeed_id = misc.uuid()
time = new Date()
obj = {project_id:projects[0], path: 'file-in-project0.txt', users:{"#{accounts[0]}":{'read':time}}}
obj.id = db.sha1(obj.project_id, obj.path)
db.user_query
account_id : accounts[0]
query : {file_use:[{id: null, project_id:null, path: null, users: null, last_edited: null}]}
changes : changefeed_id
cb : changefeed_series([
(x, cb) ->
expect(x).toEqual({ file_use: [] }) # how it starts
cb()
db.user_query(account_id: accounts[0], query: {file_use:obj}, cb: cb)
(x, cb) ->
expect(x).toEqual({action:'insert', new_val: obj})
obj.last_edited = new Date()
db.user_query(account_id: accounts[0], query: {file_use:obj}, cb: cb)
(x, cb) ->
expect(x.action).toEqual('update');
expect(x.new_val.last_edited).toEqual(obj.last_edited)
obj.users[accounts[0]].chat = new Date()
db.user_query(account_id: accounts[0], query: {file_use:obj}, cb: cb)
(x, cb) ->
expect(x.action).toEqual('update');
expect(x.new_val.users).toEqual(obj.users)
obj.users[accounts[1]] = {seen: new Date()}
db.user_query(account_id: accounts[1], query: {file_use:obj}, cb: cb)
(x, cb) ->
expect(x.action).toEqual('update');
expect(x.new_val.users).toEqual(obj.users)
obj.users[accounts[0]] = {chat: new Date(), read: new Date()}
db.user_query(account_id: accounts[0], query: {file_use:obj}, cb: cb)
(x, cb) ->
expect(x.action).toEqual('update');
expect(x.new_val.users).toEqual(obj.users)
db._query
query : "DELETE FROM file_use"
where : {'id = $': obj.id}
cb : cb
(x, cb) ->
expect(x).toEqual({action:"delete", old_val:{id:obj.id, project_id:obj.project_id}})
# Cancel the changefeed...
db.user_query_cancel_changefeed
id : changefeed_id
cb : cb
(x, cb) ->
expect(x).toEqual({action:'close'})
cb()
], done)
describe 'test changefeeds with project_log', ->
before(setup)
after(teardown)
accounts = []
projects = []
it 'create accounts and projects', (done) ->
async.series([
(cb) ->
create_accounts 2, (err, x) -> accounts=x; cb()
(cb) ->
create_projects 2, accounts[0], (err, x) -> projects.push(x...); cb(err)
(cb) ->
create_projects 1, accounts[1], (err, x) -> projects.push(x...); cb(err)
], done)
it 'tests a simple project_log changefeed on a single project', (done) ->
obj = {id:misc.uuid(), project_id:projects[0], time:new Date(), account_id:accounts[0], event:{sample:'thing'}}
changefeed_id = misc.uuid()
db.user_query
account_id : accounts[0]
query : {project_log:[{id: null, project_id:projects[0], time: null, account_id: null, event: null}]}
changes : changefeed_id
cb : changefeed_series([
(x, cb) ->
expect(x).toEqual({project_log : [] }) # how it starts
db.user_query
account_id : accounts[0]
query : {project_log:obj}
cb : cb
(x, cb) ->
expect(x).toEqual({action:'insert', new_val: obj })
db.user_query_cancel_changefeed(id : changefeed_id, cb : cb)
(x, cb) ->
# check that the next thing we get is close, not the insert we just made above.
expect(x).toEqual({action:'close'})
cb()
], done)
it "tests a project_log changefeed on all of a user's project", (done) ->
obj1 = {id:misc.uuid(), project_id:projects[1], time:new Date(), account_id:accounts[0], event:{stuff:[1,2]}}
obj2 = {id:misc.uuid(), project_id:projects[2], time:new Date(), account_id:accounts[1], event:{user:1}}
changefeed_id = misc.uuid()
db.user_query
account_id : accounts[0]
query : {project_log:[{id: null, project_id:null, time: null, account_id: null, event: null}]}
changes : changefeed_id
cb : changefeed_series([
(x, cb) ->
expect(x.project_log.length).toEqual(1) # 1 because of one added in previous test above
# insert object in projects[1]
db.user_query
account_id : accounts[0]
query : {project_log:obj1}
cb : cb
(x, cb) ->
# and see it appear
expect(x).toEqual({action:'insert', new_val: obj1 })
# insert object in projects[2], which has nothing to do with accounts[0]
db.user_query
account_id : accounts[1]
query : {project_log:obj2}
cb : (err) ->
if err
cb(err)
else
# close the changefeed
db.user_query_cancel_changefeed
id : changefeed_id
cb : cb
(x, cb) ->
# check that the next thing we get is close, not the insert we just made above.
expect(x).toEqual({action:'close'})
cb()
], done)
describe 'test time-constrained changefeed on project_log', ->
before(setup)
after(teardown)
accounts = []
projects = []
it 'create account and project', (done) ->
async.series([
(cb) ->
create_accounts 1, (err, x) -> accounts=x; cb()
(cb) ->
create_projects 1, accounts[0], (err, x) -> projects.push(x...); cb(err)
], done)
it "creates changefeed with time constraint by making two entries, one satisfying the constraint, and one not", (done) ->
changefeed_id = misc.uuid()
obj0 = {id:misc.uuid(), project_id:projects[0], time:new Date(), event:{foo:'bar0'}}
obj1 = {id:misc.uuid(), project_id:projects[0], time:misc.days_ago(2), event:{foo:'bar1'}}
obj2 = {id:misc.uuid(), project_id:projects[0], time:new Date(), event:{foo:'bar2'}}
db.user_query
account_id : accounts[0]
query : {project_log:[{id: null, project_id:null, time:{'>=':misc.days_ago(1)}, event: null}]}
changes : changefeed_id
cb : changefeed_series([
(x, cb) ->
expect(x.project_log.length).toEqual(0)
# insert object that satisfies time constraint
db.user_query(account_id: accounts[0], query: {project_log:obj0}, cb: cb)
(x, cb) ->
# and see it appear
expect(x).toEqual({action:'insert', new_val:obj0})
# insert old object that does not satisfy time constraint
db.user_query account_id: accounts[0], query: {project_log:obj1}, cb: (err) =>
if err
cb(err)
else
# then one that does satisfy constraint
db.user_query(account_id: accounts[0], query: {project_log:obj2}, cb: cb)
(x, cb) ->
# see that *only* the new one appears
expect(x).toEqual({action:'insert', new_val:obj2})
# modify the first obj so its timestamp is old, and see it gets updated to be removed (so only old_val is set)
obj3 = misc.deep_copy(obj0)
obj3.time = misc.days_ago(60)
db.user_query(account_id: accounts[0], query: {project_log:obj3}, cb:cb)
(x, cb) ->
expect(x).toEqual({action:"delete", old_val:misc.copy_without(obj0, 'event')})
cb()
], done)