Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Must have comment, right?
node_modules/
3 changes: 3 additions & 0 deletions .meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ shell-server@0.3.0
babel-runtime@1.1.1
dynamic-import@0.2.0
ecmascript
meteortesting:mocha
fds:coffeescript-share
xolvio:cleaner
7 changes: 7 additions & 0 deletions .meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ ecmascript-runtime@0.5.0
ecmascript-runtime-client@0.5.0
ecmascript-runtime-server@0.5.0
ejson@1.1.0
fds:coffeescript-share@1.0.0
geojson-utils@1.0.10
hot-code-push@1.0.4
html-tools@1.0.11
Expand All @@ -48,9 +49,14 @@ json@1.0.3
launch-screen@1.1.1
less@2.7.11
livedata@1.0.18
lmieulet:meteor-coverage@1.1.4
logging@1.1.19
meteor@1.8.2
meteor-base@1.2.0
meteorhacks:picker@1.0.3
meteortesting:browser-tests@1.0.0
meteortesting:mocha@1.1.1
meteortesting:mocha-core@1.0.1
minifier-css@1.2.16
minifier-js@2.2.2
minimongo@1.4.3
Expand Down Expand Up @@ -87,3 +93,4 @@ underscore@1.0.10
url@1.1.0
webapp@1.4.0
webapp-hashing@1.0.9
xolvio:cleaner@0.3.3
14 changes: 14 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
language: node_js
node_js:
- 8

cache:
directories:
- node_modules
- $HOME/.meteor/

before_install:
- curl https://install.meteor.com | /bin/sh

script:
- ~/.meteor/meteor test --once --driver-package meteortesting:mocha
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
codex-blackboard
================

[![Build Status](https://travis-ci.org/cjb/codex-blackboard.svg?branch=master)](https://travis-ci.org/cjb/codex-blackboard)

Meteor app for coordating solving for our MIT Mystery Hunt team. To run,
first obtain the password for our google drive account. Then:

Expand Down
10 changes: 10 additions & 0 deletions lib/imports/canonical.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict'

# canonical names: lowercases, all non-alphanumerics replaced with '_'
export default canonical = (s) ->
s = s.toLowerCase().replace(/^\s+/, '').replace(/\s+$/, '') # lower, strip
# suppress 's and 't
s = s.replace(/[\'\u2019]([st])\b/g, "$1")
# replace all non-alphanumeric with _
s = s.replace(/[^a-z0-9]+/g, '_').replace(/^_/,'').replace(/_$/,'')
return s
25 changes: 25 additions & 0 deletions lib/imports/canonical.test.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import canonical from './canonical.coffee'
import chai from 'chai'

describe 'canonical', ->
it 'strips whitespace', ->
chai.assert.equal canonical(' leading'), 'leading'
chai.assert.equal canonical('trailing '), 'trailing'
chai.assert.equal canonical('_id'), 'id'

it 'converts to lowercase', ->
chai.assert.equal canonical('HappyTime'), 'happytime'

it 'converts space to underscore', ->
chai.assert.equal canonical('sport of princesses'), 'sport_of_princesses'
chai.assert.equal canonical('sport of princesses'), 'sport_of_princesses'

it 'converts non-alphanumeric to underscore', ->
chai.assert.equal canonical("Whomst'd've"), 'whomst_d_ve'
chai.assert.equal canonical('ca$h'), 'ca_h'
chai.assert.equal canonical('command.com'), 'command_com'
chai.assert.equal canonical('2chainz'), '2chainz'

it 'deletes possessive and contraction apostrophes', ->
chai.assert.equal canonical("bill's"), 'bills'
chai.assert.equal canonical("don't"), 'dont'
18 changes: 18 additions & 0 deletions lib/imports/match.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict'

export NonEmptyString = Match.Where (x) ->
check x, String
return x.length > 0

# either an id, or an object containing an id
export IdOrObject = Match.OneOf NonEmptyString, Match.Where (o) ->
typeof o is 'object' and ((check o._id, NonEmptyString) or true)

# This is like Match.ObjectIncluding, but we don't require `o` to be
# a plain object
export ObjectWith = (pattern) ->
Match.Where (o) ->
return false if typeof(o) is not 'object'
Object.keys(pattern).forEach (k) ->
check o[k], pattern[k]
true
64 changes: 64 additions & 0 deletions lib/imports/match.test.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as match from './match.coffee'
import chai from 'chai'

describe 'NonEmptyString', ->
it 'rejects empty string', ->
chai.assert.throws ->
check '', match.NonEmptyString
, Match.Error

it 'accepts non-empty string', ->
chai.assert.doesNotThrow ->
check 'foo', match.NonEmptyString
, Match.Error

it 'rejects non-string', ->
chai.assert.throws ->
check {}, match.NonEmptyString
, Match.Error

describe 'IdOrObject', ->
it 'rejects empty string', ->
chai.assert.throws ->
check '', match.IdOrObject
, Match.Error

it 'accepts empty string', ->
chai.assert.doesNotThrow ->
check 'foo', match.IdOrObject
, Match.Error

it 'rejects empty object', ->
chai.assert.throws ->
check {}, match.IdOrObject
, Match.Error

it 'rejects object without _id', ->
chai.assert.throws ->
check {foo: 'bar'}, match.IdOrObject
, Match.Error

it 'accepts object with _id', ->
chai.assert.doesNotThrow ->
check {_id: 'fffff'}, match.IdOrObject
, Match.Error

describe 'ObjectWith', ->
it 'matches anything when empty', ->
chai.assert.doesNotThrow ->
check {foo: 'bar', baz: 3}, match.ObjectWith {}
, Match.Error

it 'matches parts', ->
chai.assert.doesNotThrow ->
check {foo: 'bar', bar: 3}, match.ObjectWith
foo: match.NonEmptyString
bar: Number
, Match.Error

it 'fails on any submatch failure', ->
chai.assert.throws ->
check {foo: '', bar: 3}, match.ObjectWith
foo: match.NonEmptyString
bar: Number
, Match.Error
21 changes: 21 additions & 0 deletions lib/imports/tags.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict'

import canonical from './canonical.coffee'
import { ObjectWith, NonEmptyString } from './match.coffee'

export getTag = (object, name) ->
(tag.value for tag in (object?.tags or []) when tag.canon is canonical(name))[0]

export isStuck = (object) ->
object? and /^stuck\b/i.test(getTag(object, 'Status') or '')

export canonicalTags = (tags, who) ->
check tags, [ObjectWith(name:NonEmptyString,value:Match.Any)]
now = Date.now()
({
name: tag.name
canon: canonical(tag.name)
value: tag.value
touched: tag.touched ? now
touched_by: tag.touched_by ? canonical(who)
} for tag in tags)
69 changes: 69 additions & 0 deletions lib/imports/tags.test.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as tags from './tags.coffee'
import chai from 'chai'

describe 'getTag', ->
it 'accepts missing object', ->
chai.assert.isUndefined tags.getTag null, 'foo'

it 'accepts missing tags', ->
chai.assert.isUndefined tags.getTag {}, 'foo'

it 'accepts empty tags', ->
chai.assert.isUndefined tags.getTag {tags: []}, 'foo'

it 'accepts nonmatching tags', ->
chai.assert.isUndefined tags.getTag {tags: [{name: 'Yo', canon: 'yo', value: 'ho ho'}]}, 'foo'

it 'accepts matching tags', ->
chai.assert.equal tags.getTag({tags: [{name: 'Yo', canon: 'yo', value: 'ho ho'}]}, 'yo'), 'ho ho'

it 'canonicalizes tags', ->
chai.assert.equal tags.getTag({tags: [{name: 'Yo', canon: 'yo', value: 'ho ho'}]}, 'yO'), 'ho ho'

describe 'isStuck', ->
it 'accepts missing object', ->
chai.assert.isFalse tags.isStuck null

it 'accepts missing tags', ->
chai.assert.isFalse tags.isStuck {}

it 'accepts empty tags', ->
chai.assert.isFalse tags.isStuck {tags: []}

it 'ignores other tags', ->
chai.assert.isFalse tags.isStuck {tags: [{name: 'Yo', canon: 'yo', value: 'ho ho'}]}

it 'ignores nonstuck status', ->
chai.assert.isFalse tags.isStuck {tags: [{name: 'Status', canon: 'status', value: 'making progress'}]}

it 'matches stuck status', ->
chai.assert.isTrue tags.isStuck {tags: [{name: 'Status', canon: 'status', value: 'stuck'}]}

it 'matches verbose stuck status', ->
chai.assert.isTrue tags.isStuck {tags: [{name: 'Status', canon: 'status', value: 'Stuck to the wall'}]}

describe 'canonicalTags', ->
it 'requires list', ->
chai.assert.throws ->
tags.canonicalTags null, 'torgen'
chai.assert.throws ->
tags.canonicalTags {}, 'torgen'
chai.assert.deepEqual tags.canonicalTags([], 'torgen'), []

it 'fills entries', ->
pre = Date.now()
[foo, baz] = tags.canonicalTags [{name: 'Foo', value: 'bar'}, {name: 'BaZ', value: 'qux'}], 'Torgen'
chai.assert.include foo, {name: 'Foo', canon: 'foo', value: 'bar', touched_by: 'torgen'}
chai.assert.isAtLeast foo.touched, pre
chai.assert.include baz, {name: 'BaZ', canon: 'baz', value: 'qux', touched_by: 'torgen'}
chai.assert.isAtLeast baz.touched, pre

it 'preserves touched', ->
pre = Date.now() - 5
chai.assert.deepEqual(
tags.canonicalTags([{name: 'Foo', value: 'bar', touched: pre}], 'torgen'),
[{name: 'Foo', canon: 'foo', value: 'bar', touched: pre, touched_by: 'torgen'}])

it 'preserves touched_by', ->
[tag] = tags.canonicalTags [{name: 'Foo', value: 'bar', touched_by: 'cscott'}], 'torgen'
chai.assert.include tag, {name: 'Foo', canon: 'foo', value: 'bar', touched_by: 'cscott'}
102 changes: 102 additions & 0 deletions lib/methods/addIncorrectAnswer.test.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict'

# Will access contents via share
import '../model.coffee'
import chai from 'chai'
import sinon from 'sinon'
import { resetDatabase } from 'meteor/xolvio:cleaner'

model = share.model

describe 'addIncorrectAnswer', ->
clock = null

beforeEach ->
clock = sinon.useFakeTimers(7)

afterEach ->
clock.restore()

beforeEach ->
resetDatabase()

it 'fails on non-puzzle', ->
id = model.Nicks.insert
name: 'Torgen'
canon: 'torgen'
tags: [{name: 'Answer', canon: 'answer', value: 'knock knock', touched: 1, touched_by: 'torgen'}]
chai.assert.throws ->
Meteor.call 'addIncorrectAnswer',
type: 'nicks'
target: id
who: 'cjb'
answer: 'foo'
, Match.Error

['roundgroups', 'rounds', 'puzzles'].forEach (type) =>
describe "on #{model.pretty_collection(type)}", ->
it 'fails when it doesn\'t exist', ->
chai.assert.throws ->
Meteor.call 'newCallIn',
type: type
target: 'something'
answer: 'precipitate'
who: 'torgen'
, Meteor.Error

describe 'which exists', ->
id = null
beforeEach ->
id = model.collection(type).insert
name: 'Foo'
canon: 'foo'
created: 1
created_by: 'cscott'
touched: 2
touched_by: 'torgen'
solved: null
solved_by: null
tags: [{name: 'Status', canon: 'status', value: 'stuck', touched: 2, touched_by: 'torgen'}]
incorrectAnswers: [{answer: 'qux', who: 'torgen', timestamp: 2, backsolve: false, provided: false}]
model.CallIns.insert
type: type
target: id
name: 'Foo'
answer: 'flimflam'
created: 4
created_by: 'cjb'
Meteor.call 'addIncorrectAnswer',
type: type
target: id
who: 'cjb'
answer: 'flimflam'

it 'appends answer', ->
doc = model.collection(type).findOne id
chai.assert.lengthOf doc.incorrectAnswers, 2
chai.assert.include doc.incorrectAnswers[1],
answer: 'flimflam'
who: 'cjb'
timestamp: 7
backsolve: false
provided: false

it 'doesn\'t touch', ->
doc = model.collection(type).findOne id
chai.assert.include doc,
touched: 2
touched_by: 'torgen'

it 'oplogs', ->
o = model.Messages.find(room_name: 'oplog/0').fetch()
chai.assert.lengthOf o, 1
chai.assert.include o[0],
type: type
id: id
stream: 'callins'
nick: 'cjb'
# oplog is lowercase
chai.assert.include o[0].body, 'flimflam', 'message'

it 'deletes callin', ->
chai.assert.lengthOf model.CallIns.find().fetch(), 0
Loading