diff --git a/.meteor/packages b/.meteor/packages
index ccc93d8..6bc6b6d 100644
--- a/.meteor/packages
+++ b/.meteor/packages
@@ -20,3 +20,5 @@ shell-server@0.2.3 # Server-side component of the `meteor shell` comm
accounts-password
session
fourseven:scss
+practicalmeteor:mocha
+react-meteor-data
diff --git a/.meteor/versions b/.meteor/versions
index 66ab2f0..e696338 100644
--- a/.meteor/versions
+++ b/.meteor/versions
@@ -14,6 +14,7 @@ caching-compiler@1.1.9
caching-html-compiler@1.1.2
callback-hook@1.0.10
check@1.2.5
+coffeescript@1.0.17
ddp@1.2.5
ddp-client@1.3.4
ddp-common@1.2.8
@@ -54,9 +55,15 @@ npm-bcrypt@0.9.2
npm-mongo@2.2.24
observe-sequence@1.0.16
ordered-dict@1.0.9
+practicalmeteor:chai@2.1.0_1
+practicalmeteor:loglevel@1.2.0_2
+practicalmeteor:mocha@2.4.5_6
+practicalmeteor:mocha-core@1.0.1
+practicalmeteor:sinon@1.14.1_2
promise@0.8.8
random@1.0.10
rate-limit@1.0.8
+react-meteor-data@0.2.11
reactive-dict@1.1.8
reactive-var@1.0.11
reload@1.1.11
@@ -75,6 +82,8 @@ templating@1.3.2
templating-compiler@1.3.2
templating-runtime@1.3.2
templating-tools@1.1.2
+tmeasday:check-npm-versions@0.2.0
+tmeasday:test-reporter-helpers@0.2.1
tracker@1.1.3
ui@1.0.13
underscore@1.0.10
diff --git a/client/main.html b/client/main.html
index 46c2621..3706ac5 100644
--- a/client/main.html
+++ b/client/main.html
@@ -1,5 +1,5 @@
- Meteor BoilerPlate
+ Todo Apps
diff --git a/import/api/notes.js b/import/api/notes.js
new file mode 100644
index 0000000..6e8ddfc
--- /dev/null
+++ b/import/api/notes.js
@@ -0,0 +1,74 @@
+import {Meteor} from 'meteor/meteor'
+import {Mongo} from 'meteor/mongo'
+
+import moment from 'moment'
+import SimpleSchema from 'simpl-schema';
+
+export const Notes = new Mongo.Collection('notes')
+
+if(Meteor.isServer){
+ Meteor.publish('notes', function(){
+ return Notes.find({userId:this.userId})
+ })
+}
+
+Meteor.methods({
+ 'notes.insert'(){
+ if(!this.userId){
+ throw new Meteor.Error('not-authenticated')
+ }
+ return Notes.insert({
+ title:'',
+ body:'',
+ userId: this.userId,
+ updatedAt: moment().valueOf()
+ })
+ },
+
+ 'notes.remove'(_id){
+ if(!this.userId){
+ throw new Meteor.Error('not-authenticated')
+ }
+ try{
+ new SimpleSchema({
+ _id:{
+ type:String,
+ min:1
+ }
+ }).validate({_id})
+ }catch(e){
+ console.log('e',e,e.message);
+ throw new Meteor.Error(400, e.message)
+ }
+
+ Notes.remove({_id, userId:this.userId});
+ },
+ 'notes.update'(_id, updates){
+ if(!this.userId){
+ throw new Meteor.Error('not-authenticated')
+ }
+
+ try{
+ new SimpleSchema({
+ _id:{
+ type:String,
+ min:1
+ },
+ title:{
+ type:String,
+ optional:true
+ },
+ body:{
+ type:String,
+ optional:true
+ }
+ }).validate({_id, ...updates})
+ }catch(e){
+ console.log('e',e,e.message);
+ throw new Meteor.Error(400, e.message)
+ }
+
+ Notes.update({_id, userId:this.userId },{$set:{updatedAt: moment().valueOf(), ...updates}})
+
+ }
+})
diff --git a/import/api/notes.test.js b/import/api/notes.test.js
new file mode 100644
index 0000000..2c0f64c
--- /dev/null
+++ b/import/api/notes.test.js
@@ -0,0 +1,116 @@
+import {Meteor} from 'meteor/meteor'
+import expect from 'expect';
+
+import {Notes} from './notes'
+
+if(Meteor.isServer){
+ describe('notes',function(){
+
+ const noteOne = {
+ _id:'testNoteId1',
+ title:'My Title',
+ body:'My body for note',
+ userId: 'testUserId1',
+ updatedAt: 0
+ }
+
+ const noteTwo = {
+ _id:'testNoteId2',
+ title:'My Title 2',
+ body:'My body for note 2',
+ userId: 'testUserId2',
+ updatedAt: 0
+ }
+
+ beforeEach(function(){
+ Notes.remove({});
+ Notes.insert(noteOne)
+ Notes.insert(noteTwo)
+ })
+
+ it('should insert new note', function(){
+ const userId = 'testId'
+ const _id = Meteor.server.method_handlers['notes.insert'].apply({userId:'testId'})
+
+ expect(Notes.findOne({_id, userId})).toExist();
+ })
+
+ it('should not insert note if not authentciated',function(){
+ expect(()=>{
+ Meteor.server.method_handlers['notes.insert']();
+ }).toThrow();
+ })
+
+ it('should remove note', function(){
+ Meteor.server.method_handlers['notes.remove'].apply({userId:noteOne.userId},[noteOne._id])
+
+ expect(Notes.findOne({_id: noteOne._id})).toNotExist();
+ })
+
+ it('should not remove note if unauthenticated', function(){
+ expect(()=>{
+ Meteor.server.method_handlers['notes.remove'].apply({},[noteOne._id])
+ }).toThrow();
+ })
+
+ it('should not remove note if invalid _id', function(){
+ expect(()=>{
+ Meteor.server.method_handlers['notes.remove'].apply({userId:noteOne.userId},[])
+ }).toThrow();
+ })
+
+ it('should update note', function(){
+ const title = 'This is an updated title'
+
+ Meteor.server.method_handlers['notes.update'].apply({userId:noteOne.userId},[noteOne._id, {title}])
+
+ const note = Notes.findOne(noteOne._id)
+
+ expect(note.updatedAt).toBeGreaterThan(0)
+ expect(note).toInclude({title,body:noteOne.body})
+ })
+
+ it('should throw err if extra updates', function(){
+ expect(()=>{
+ Meteor.server.method_handlers['notes.remove'].apply({userId:noteOne.userId},[noteOne._id, title:'new title', name:'name'])
+ }).toThrow();
+ })
+
+ it('should not update note if user is not creater', function(){
+ const title = 'This is an updated title'
+ Meteor.server.method_handlers['notes.remove'].apply({userId:'testid'},[noteOne._id, {title}])
+ const note = Notes.findOne(noteOne._id)
+ expect(note).toInclude(noteOne)
+ })
+
+ it('should not update note if unauthenticated', function(){
+ expect(()=>{
+ Meteor.server.method_handlers['notes.update'].apply({},[noteOne._id])
+ }).toThrow();
+ })
+
+ it('should not update note if invalid _id', function(){
+ expect(()=>{
+ Meteor.server.method_handlers['notes.update'].apply({userId:noteOne.userId},[])
+ }).toThrow();
+ })
+
+ it('should return a users notes', function(){
+ const res = Meteor.server.publish_handlers.notes.apply({userId:'testUserId2'})
+ const notes = res.fetch();
+
+ expect(notes.length).toBe(1)
+ expect(notes[0]).toEqual(noteTwo)
+ })
+
+ it('should return zero notes', function(){
+ const res = Meteor.server.publish_handlers.notes.apply({userId:'testUserId3'})
+ const notes = res.fetch();
+
+ expect(notes.length).toBe(0)
+ })
+
+
+
+ })
+}
diff --git a/import/api/users.js b/import/api/users.js
index 1ba27dc..3ae7774 100644
--- a/import/api/users.js
+++ b/import/api/users.js
@@ -2,19 +2,22 @@ import { Meteor } from 'meteor/meteor';
import SimpleSchema from 'simpl-schema';
import {Accounts} from 'meteor/accounts-base';
-// Accounts.validateNewUser((user)=>{
-// const email = user.emails[0].address;
-// try{
-// new SimpleSchema({
-// email:{
-// type:String,
-// regEx: SimpleSchema.RegEx.Email
-// }
-// }).validate({email})
-// } catch(e){
-// console.log('e',e,e.message);
-// throw new Meteor.Error(400, e.message)
-// }
-//
-// return true;
-// })
+export const validateNewUser = (user)=>{
+ const email = user.emails[0].address;
+ try{
+ new SimpleSchema({
+ email:{
+ type:String,
+ regEx: SimpleSchema.RegEx.Email
+ }
+ }).validate({email})
+ }catch(e){
+ console.log('e',e,e.message);
+ throw new Meteor.Error(400, e.message)
+ }
+
+ return true;
+}
+if(Meteor.isServer){
+ Accounts.validateNewUser(validateNewUser)
+}
diff --git a/import/api/users.test.js b/import/api/users.test.js
new file mode 100644
index 0000000..2506cf3
--- /dev/null
+++ b/import/api/users.test.js
@@ -0,0 +1,63 @@
+import {Meteor} from 'meteor/meteor'
+import expect from 'expect'
+
+import {validateNewUser} from './users'
+
+if(Meteor.isServer){
+ describe('users', function(){
+ it('should allow valid email address',function(){
+ const testUser = {
+ emails:[
+ {address:'Test@example.com'}
+ ]
+ }
+ const res = validateNewUser(testUser)
+
+ expect(res).toBe(true);
+ })
+
+ it('should reject invalid email', function(){
+ const testUser={
+ emails:[
+ {address:'testtests'}
+ ]
+ }
+
+ expect(()=>{
+ validateNewUser(testUser);
+ }).toThrow();
+ })
+ })
+
+}
+
+// const add = (a,b) => {
+// if (typeof b !== 'number') return a + a;
+// else return a+b;
+// }
+//
+// const square = (a)=>{
+// return a*a;
+// }
+//
+// describe('add', function(){
+// it('should add two number', function(){
+// const res = add(3,4)
+// // if(res!==7) throw new Error('Sum was not equal to expected value')
+// expect(res).toBe(20);
+// });
+//
+// it('should double a single number' , function(){
+// const res = add(33);
+// // if(res!==88) throw new Error('Number was not doubled')
+// expect(res).toBe(66);
+// })
+//
+// })
+//
+// describe('square', function(){
+// it('should square a number', function(){
+// const res = square(11);
+// expect(res).toBe(121);
+// })
+// })
diff --git a/import/ui/dashboard.js b/import/ui/dashboard.js
index c23ec99..28d77a3 100644
--- a/import/ui/dashboard.js
+++ b/import/ui/dashboard.js
@@ -1,13 +1,14 @@
import React from 'react';
import PrivateHeader from './privateHeader';
+import NoteList from './noteList'
export default () =>{
return(
- Dasboard page content
+
)
diff --git a/import/ui/login.js b/import/ui/login.js
index 01fc5ba..5f1a23b 100644
--- a/import/ui/login.js
+++ b/import/ui/login.js
@@ -1,8 +1,10 @@
import {Meteor} from 'meteor/meteor';
import React from 'react';
import {Link} from 'react-router-dom';
+import {createContainer} from 'meteor/react-meteor-data'
+import PropTypes from 'prop-types'
-export default class Login extends React.Component{
+export class Login extends React.Component{
constructor(props){
super(props);
@@ -17,7 +19,7 @@ export default class Login extends React.Component{
let email = this.refs.email.value.trim();
let password = this.refs.password.value.trim();
- Meteor.loginWithPassword({email},password,(err)=>{
+ this.props.loginWithPassword({email},password,(err)=>{
err? this.setState({error: err.reason}):this.setState({error: ""})
})
@@ -43,3 +45,13 @@ export default class Login extends React.Component{
)
}
}
+
+Login.propTypes={
+ loginWithPassword:PropTypes.func
+}
+
+export default createContainer(()=>{
+ return{
+ loginWithPassword: Meteor.loginWithPassword()
+ }
+}, Login)
diff --git a/import/ui/login.test.js b/import/ui/login.test.js
new file mode 100644
index 0000000..c894810
--- /dev/null
+++ b/import/ui/login.test.js
@@ -0,0 +1,52 @@
+import {Meteor} from 'meteor/meteor'
+import React from 'react'
+import expect from 'expect'
+import ReactTestUtils from 'react-dom/test-utils'
+import {mount} from 'enzyme'
+
+import {Login} from './login'
+
+if(Meteor.isClient){
+ describe('Login', function(){
+
+ it('should show err message', function(){
+ const error = 'err msg'
+ const wrapper = mount({}}/>)
+
+ wrapper.setState({error})
+ const err = wrapper.find('p').text()
+ expect(err).toBe(error)
+
+ wrapper.setState({error:""})
+ const errr = wrapper.find('p').length
+ expect(errr).toBe(0)
+ })
+
+ it('should call loginWithPassword with the form data', function(){
+ const email = 'test@test.com'
+ const password = '123'
+ const spy = expect.createSpy()
+ const wrapper = mount()
+
+ wrapper.ref('email').node.value = email;
+ wrapper.ref('password').node.value = password;
+ wrapper.find('form').simulate('submit');
+
+ expect(spy.calls[0].arguments[0]).toEqual({email})
+ expect(spy.calls[0].argunments[1]).toBe(password)
+ })
+
+ it('should call loginWithPassword callback errors', function(){
+ const spy = expect.createSpy();
+ const wrapper = mount()
+
+ wrapper.find('form').simulate('submit')
+
+ spy.calls[0].argunments[2]({})
+ expect(wrapper).state('error').toNotBe('')
+
+ spy.calls[0].argunments[2]()
+ expect(wrapper).state('error').toNotBe('')
+ })
+ })
+}
diff --git a/import/ui/noteList.js b/import/ui/noteList.js
new file mode 100644
index 0000000..c2fda2a
--- /dev/null
+++ b/import/ui/noteList.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import {Meteor} from 'meteor/meteor'
+import {createContainer} from 'meteor/react-meteor-data'
+import PropTypes from 'prop-types'
+
+import {Notes} from '../api/notes'
+import {NoteListHeader} from './noteListHeader'
+
+export const NoteList = (props) =>{
+ return(
+
+
+ NotesList {props.notes.length}
+
+ )
+}
+
+NoteList.PropTypes={
+ notes:PropTypes.func
+}
+
+export default createContainer(()=>{
+ Meteor.subscribe('notes');
+
+ return{
+ notes: Notes.find().fetch()
+ }
+},NoteList)
diff --git a/import/ui/noteListHeader.js b/import/ui/noteListHeader.js
new file mode 100644
index 0000000..0fe53db
--- /dev/null
+++ b/import/ui/noteListHeader.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import {Meteor} from 'meteor/meteor'
+import {createContainer} from 'meteor/react-meteor-data'
+import PropTypes from 'prop-types'
+
+export class NoteListHeader extends React.Component{
+ constructor(props){
+ super(props)
+ this.state={
+
+ }
+ }
+
+ onClick(e){
+ this.props.meteorCall('notes.insert', (err)=>{
+ if(err) throw err;
+ })
+ }
+
+ render(){
+ return(
+
+
+
+
+ )
+ }
+}
+
+NoteListHeader.propTypes={
+ meteorCall:PropTypes.func
+}
+
+export default createContainer(()=>{
+ return{
+ meteorCall:Meteor.call
+ }
+}, NoteListHeader)
diff --git a/import/ui/privateHeader.js b/import/ui/privateHeader.js
index b59209a..22ff71c 100644
--- a/import/ui/privateHeader.js
+++ b/import/ui/privateHeader.js
@@ -1,16 +1,27 @@
import React from 'react';
import {Accounts} from 'meteor/accounts-base';
+import {createContainer} from 'meteor/react-meteor-data'
+import PropTypes from 'prop-types'
//stateless functional component
-const PrivateHeader = (props)=>{
+export const PrivateHeader = (props)=>{
return (
{props.title}
-
+
)
}
-export default PrivateHeader;
+PrivateHeader.proptypes={
+ title: PropTypes.string,
+ handleLogout: PropTypes.func
+}
+
+export default createContainer(()=>{
+ return{
+ handleLogout: ()=>Accounts.logout()
+ };
+}, PrivateHeader);
diff --git a/import/ui/privateHeader.test.js b/import/ui/privateHeader.test.js
new file mode 100644
index 0000000..b9bb899
--- /dev/null
+++ b/import/ui/privateHeader.test.js
@@ -0,0 +1,34 @@
+import {Meteor} from 'meteor/meteor'
+import React from 'react'
+import expect from 'expect'
+import {mount} from 'enzyme'
+
+import {PrivateHeader} from './privateHeader'
+
+if(Meteor.isClient){
+ describe('PrivateHeader', function(){
+ it('should set button text to logout', function(){
+ const wrapper = mount()
+ const buttonText = wrapper.find('button').text()
+
+ expect(buttonText).toBe('Logout')
+ });
+
+ it('should use title prop as h1 text', function(){
+ const title = 'Test title here'
+ const wrapper = mount()
+ const h1Title = wrapper.find('h1').text()
+
+ expect(h1Title).toBe(title)
+ })
+
+ it('should call handleLogout function', function(){
+ const spy = expect.createSpy();
+ const wrapper = mount()
+
+ wrapper.find('button').simulate('click');
+
+ expect(spy).toHaveBeenCalled();
+ })
+ })
+}
diff --git a/import/ui/signup.js b/import/ui/signup.js
index de61198..fb86380 100644
--- a/import/ui/signup.js
+++ b/import/ui/signup.js
@@ -1,8 +1,10 @@
import React from 'react';
import {Link} from 'react-router-dom';
import {Accounts} from 'meteor/accounts-base';
+import {createContainer} from 'meteor/react-meteor-data'
+import PropTypes from 'prop-types'
-export default class Signup extends React.Component{
+export class Signup extends React.Component{
constructor(props){
super(props);
@@ -19,7 +21,7 @@ export default class Signup extends React.Component{
if(password.length<8) return this.setState({error:"Password must be at least 8 characters"})
- Accounts.createUser({email, password}, (err)=>{
+ this.props.createUser({email, password}, (err)=>{
err? this.setState({error: err.reason}):this.setState({error: ""})
})
@@ -45,3 +47,13 @@ export default class Signup extends React.Component{
)
}
}
+
+Signup.propTypes={
+ createUser:PropTypes.func
+}
+
+export default createContainer(()=>{
+ return{
+ createUser: Accounts.createUser
+ }
+}, Signup)
diff --git a/import/ui/signup.test.js b/import/ui/signup.test.js
new file mode 100644
index 0000000..6f2c9f6
--- /dev/null
+++ b/import/ui/signup.test.js
@@ -0,0 +1,51 @@
+import {Meteor} from 'meteor/meteor'
+import React from 'react'
+import expect from 'expect'
+import {mount} from 'enzyme'
+
+import {Signup} from './signup'
+
+if(Meteor.isClient){
+ describe('Signup', function(){
+
+ it('should show err message', function(){
+ const error = 'err msg'
+ const wrapper = mount({}}/>)
+
+ wrapper.setState({error})
+ const err = wrapper.find('p').text()
+ expect(err).toBe(error)
+
+ wrapper.setState({error:""})
+ const errr = wrapper.find('p').length
+ expect(errr).toBe(0)
+ })
+
+ it('should call createUser with the form data', function(){
+ const email = 'test@test.com'
+ const password = '123'
+ const spy = expect.createSpy()
+ const wrapper = mount()
+
+ wrapper.ref('email').node.value = email;
+ wrapper.ref('password').node.value = password;
+ wrapper.find('form').simulate('submit');
+
+ expect(spy.calls[0].arguments[0]).toEqual({email})
+ expect(spy.calls[0].argunments[1]).toBe(password)
+ })
+
+ it('should call createUser callback errors', function(){
+ const spy = expect.createSpy();
+ const wrapper = mount()
+
+ wrapper.find('form').simulate('submit')
+
+ spy.calls[0].argunments[2]({})
+ expect(wrapper).state('error').toNotBe('')
+
+ spy.calls[0].argunments[2]()
+ expect(wrapper).state('error').toNotBe('')
+ })
+ })
+}
diff --git a/package.json b/package.json
index ca433c5..860815d 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,9 @@
{
- "name": "short-lnk",
+ "name": "notes",
"private": true,
"scripts": {
- "start": "meteor run"
+ "start": "meteor run",
+ "test": "meteor test --driver-package=practicalmeteor:mocha"
},
"dependencies": {
"babel-runtime": "^6.20.0",
@@ -11,12 +12,19 @@
"meteor-node-stubs": "~0.2.4",
"moment": "^2.18.1",
"react": "^15.5.4",
+ "react-addons-pure-render-mixin": "^15.5.2",
"react-dom": "^15.5.4",
"react-router": "^4.1.1",
"react-router-dom": "^4.1.1",
"simpl-schema": "^0.3.0"
},
- "engines":{
- "node":"4.8.2"
+ "engines": {
+ "node": "4.8.2"
+ },
+ "devDependencies": {
+ "enzyme": "^2.8.2",
+ "expect": "^1.20.2",
+ "react-addons-test-utils": "^15.5.1",
+ "react-test-renderer": "^15.5.4"
}
}
diff --git a/server/main.js b/server/main.js
index 7063a46..d704946 100644
--- a/server/main.js
+++ b/server/main.js
@@ -3,6 +3,7 @@ import {WebApp} from 'meteor/webapp'
import '../import/api/users'
+import '../import/api/notes'
Meteor.startup(() => {