Unit tests and some consequent fixes
6 files changed, 310 insertions(+), 155 deletions(-)

M cal.go
M cal_test.go
M db.go
M db_test.go
M main.go
M main_test.go
M cal.go +5 -4
@@ 11,14 11,14 @@ import (
 
 type Calendar interface {
 	Stats() CalStats
-	ForEachIcs(func(io.Reader) error)
+	ForEachIcs(func(io.Reader) error) error
 }
 
 type CalStats struct {
 	Entries int
 }
 
-func NewCal(path string) Calendar {
+func OpenCal(path string) Calendar {
 	return calendar{path}
 }
 

          
@@ 36,9 36,9 @@ func (c calendar) Stats() CalStats {
 	}
 }
 
-func (c calendar) ForEachIcs(f func(io.Reader) error) {
+func (c calendar) ForEachIcs(f func(io.Reader) error) error {
 	// drop through to operate on calendar
-	filepath.WalkDir(c.path, func(p string, d fs.DirEntry, err error) error {
+	err := filepath.WalkDir(c.path, func(p string, d fs.DirEntry, err error) error {
 		if err != nil {
 			return nil
 		}

          
@@ 56,4 56,5 @@ func (c calendar) ForEachIcs(f func(io.R
 		}
 		return nil
 	})
+	return err
 }

          
M cal_test.go +159 -43
@@ 1,74 1,190 @@ 
 package main
 
 import (
+	"bufio"
 	"io"
+	"io/ioutil"
+	"os"
 	"reflect"
+	"strings"
 	"testing"
+	"text/template"
+	"time"
 )
 
-func TestNewCal(t *testing.T) {
-	type args struct {
-		path string
-	}
-	tests := []struct {
-		name string
-		args args
-		want Calendar
-	}{
-	// TODO: Add test cases.
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := NewCal(tt.args.path); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("NewCal() %s = %v, want %v", tt.name, got, tt.want)
-			}
-		})
-	}
-}
-
 func Test_calendar_Stats(t *testing.T) {
-	type fields struct {
-		path string
-	}
 	tests := []struct {
 		name   string
-		fields fields
+		count   int
 		want   CalStats
 	}{
-	// TODO: Add test cases.
+		{"is empty", 0, CalStats{0}},
+		{"not empty", 5, CalStats{5}},
 	}
+	cn := "testcal"
+	os.RemoveAll(cn)
 	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			c := calendar{
-				path: tt.fields.path,
-			}
+		t.Run  (tt.name, func(t *testing.T) {
+			setupTestDir(cn, tt.count)
+			defer os.RemoveAll(cn)
+			c := OpenCal(cn)
 			if got := c.Stats(); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("calendar.Stats() %s = %v, want %v", tt.name, got, tt.want)
+				t.Errorf("%s = %v, want %v", tt.name, got, tt.want)
 			}
 		})
 	}
 }
 
 func Test_calendar_ForEachIcs(t *testing.T) {
-	type fields struct {
-		path string
-	}
-	type args struct {
-		f func(io.Reader) error
-	}
+	var ctr int
+	var testN int
 	tests := []struct {
 		name   string
-		fields fields
-		args   args
+		count int
+		wants  int
+		args   func (io.Reader) error
 	}{
-	// TODO: Add test cases.
+		{ "count", 10, 10, func(f io.Reader) error {
+			ctr++
+			return nil
+		}},
+		{ "lines", 1, 34, func(f io.Reader) error {
+			scanner := bufio.NewScanner(f)
+			for scanner.Scan() {
+				ctr++
+			}
+			if err := scanner.Err(); err != nil {
+				return err
+			}
+			return nil
+		}},
+		{ "UID", 3, 3, func(f io.Reader) error {
+			s := makeId(testN)
+			bs, err := ioutil.ReadAll(f)
+			if err != nil {
+				return err
+			}
+			strings.Contains(string(bs), s)
+			return nil
+		}},
 	}
+	cn := "testcal"
+	os.RemoveAll(cn)
 	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			c := calendar{
-				path: tt.fields.path,
+		t.Run  (tt.name, func(t *testing.T) {
+			if err := setupTestDir(cn, tt.count); err != nil {
+				t.Errorf("%s = setting up test cal directory: %s", cn, err)
 			}
-			c.ForEachIcs(tt.args.f)
+			defer os.RemoveAll(cn)
+			c := OpenCal(cn)
+			ctr = 0
+			if err := c.ForEachIcs(tt.args); err != nil {
+				t.Errorf("%s = error running test %v", tt.name, err)
+			}
 		})
 	}
 }
+
+func setupTestDir(cn string, sz int) error {
+	err := os.Mkdir(cn, 0700)
+	if err != nil {
+		return err
+	}
+	// Make some entries
+	for ctr := 0; ctr < sz; ctr++ {
+		f, err := os.CreateTemp(cn, "test.*.ics")
+		if err != nil {
+			return err
+		}
+		makeData(f, makeId(ctr), "summary", "location", time.Now(), time.Now())
+		f.Close()
+
+	}
+	return nil
+}
+
+func makeId(k int) string {
+	rv := make([]byte, 37)
+	for i := 0; i < 37; i++ {
+		rv[i] = byte(k)
+	}
+	return string(rv)
+}
+func makeSummary(k int) string {
+	l := byte(('a' + k) % 26)
+	rv := make([]byte, 37)
+	for i := 0; i < 37; i++ {
+		rv[i] = l
+	}
+	return string(rv)
+}
+func makeLocation(k int) string {
+	l := byte(('A' + k) % 26)
+	rv := make([]byte, 11)
+	for i := 0; i < 11; i++ {
+		rv[i] = l
+	}
+	return string(rv)
+}
+func makeStart(k int) time.Time {
+	return time.Unix(int64(k), int64(k))
+}
+func makeEnd(k int) time.Time {
+	return time.Unix(int64(k+1), int64(k+1))
+}
+
+func makeData(fout io.Writer, uid, summary, location string, start, end time.Time) error {
+	t, e := template.New("data").Parse(DATA)
+	if e != nil {
+		return e
+	}
+	return t.Execute(fout, struct {
+		       Uid string
+		       Summary string
+		       Location string
+		       Start time.Time
+		       End time.Time
+		} {
+			Uid: uid,
+			Summary: summary,
+			Location: location,
+			Start: start,
+			End: end,
+		},
+	)
+}
+
+var DATA string = `BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN
+BEGIN:VTIMEZONE
+TZID:America/Chicago
+BEGIN:DAYLIGHT
+DTSTART;VALUE=DATE-TIME:20190310T030000
+TZNAME:CDT
+TZOFFSETFROM:-0600
+TZOFFSETTO:-0500
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART;VALUE=DATE-TIME:20191103T010000
+TZNAME:CST
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0600
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+SUMMARY:{{.Summary}}
+DTSTART;TZID=America/Chicago;VALUE=DATE-TIME:{{.Start}}
+DTEND;TZID=America/Chicago;VALUE=DATE-TIME:{{.End}}
+DTSTAMP;VALUE=DATE-TIME:20190816T143301Z
+UID:{{.Uid}}
+SEQUENCE:0
+CATEGORIES:
+LOCATION:{{.Location}}
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:
+TRIGGER:-PT1H
+END:VALARM
+END:VEVENT
+END:VCALENDAR`

          
M db.go +26 -4
@@ 14,22 14,35 @@ import (
 type Db interface {
 	Stats() DbStats
 	UpdateInvites(string, []string) error
+	Error() error
 }
 
-// TODO fill in DbStats
 type DbStats struct {
 	Entries int
 }
 
 type db struct {
 	path string
+	err  error
 }
 
-func NewDb(path string) Db {
-	return db{path}
+func OpenDb(path string) Db {
+	fout, err := os.Open(path)
+	if err != nil {
+		if errors.Is(err, os.ErrNotExist) {
+			// Set up the test DB
+			fout, err = os.Create(path)
+			if err != nil {
+				return db{err: err}
+			}
+		} else {
+			return db{err: err}
+		}
+	}
+	fout.Close()
+	return db{path: path}
 }
 
-// TODO implement dbStats()
 func (d db) Stats() DbStats {
 	rs, err := d.readDb()
 	if err != nil {

          
@@ 41,6 54,10 @@ func (d db) Stats() DbStats {
 	}
 }
 
+func (d db) Error() error {
+	return d.err
+}
+
 // persistInvites saves the invite list of the ICS UUID to the database.
 // It replaces any existing record for that UUID.
 func (d db) UpdateInvites(id string, as []string) error {

          
@@ 59,6 76,7 @@ func (d db) UpdateInvites(id string, as 
 		return err
 	}
 	defer lock.Unlock()
+	defer os.Remove(lock.Path())
 
 	rs, err := d.readDb()
 	if err != nil {

          
@@ 69,6 87,8 @@ func (d db) UpdateInvites(id string, as 
 	for _, r := range rs {
 		if r[0] == id {
 			r[1] = sas
+			found = true
+			break
 		}
 	}
 	if !found {

          
@@ 92,11 112,13 @@ func (d db) save(rs [][]string) error {
 	}
 	defer fout.Close()
 	csvw := csv.NewWriter(fout)
+	csvw.Comma = '\t'
 	err = csvw.WriteAll(rs)
 	if err != nil {
 		return err
 	}
 	csvw.Flush()
+	os.Rename(foutn, d.path)
 	return nil
 }
 

          
M db_test.go +111 -88
@@ 1,46 1,39 @@ 
 package main
 
 import (
+	"io/ioutil"
+	"os"
 	"reflect"
 	"testing"
 )
 
-func TestNewDb(t *testing.T) {
-	type args struct {
-		path string
-	}
-	tests := []struct {
-		name string
-		args args
-		want Db
-	}{
-	// TODO: Add test cases.
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := NewDb(tt.args.path); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("NewDb() %s = %v, want %v", tt.name, got, tt.want)
-			}
-		})
-	}
-}
-
 func Test_db_Stats(t *testing.T) {
 	type fields struct {
 		path string
+		size int
 	}
 	tests := []struct {
 		name   string
 		fields fields
 		want   DbStats
 	}{
-	// TODO: Add test cases.
+		{"is empty", fields{"testdb", 0}, DbStats{0}},
+		{"not empty", fields{"testdb", 5}, DbStats{5}},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			d := db{
-				path: tt.fields.path,
+			td, err := ioutil.TempFile("", tt.fields.path)
+			if err != nil {
+				t.Errorf("db.Stats() %s = setting up DB: %s", tt.name, err)
+				return
 			}
+			defer os.Remove(td.Name())
+			// Make some entries
+			for ctr := 0; ctr < tt.fields.size; ctr++ {
+				td.WriteString("dummy\n")
+			}
+			td.Close()
+			d := OpenDb(td.Name())
 			if got := d.Stats(); !reflect.DeepEqual(got, tt.want) {
 				t.Errorf("db.Stats() %s = %v, want %v", tt.name, got, tt.want)
 			}

          
@@ 49,85 42,115 @@ func Test_db_Stats(t *testing.T) {
 }
 
 func Test_db_UpdateInvites(t *testing.T) {
-	type fields struct {
-		path string
-	}
 	type args struct {
 		id string
 		as []string
 	}
 	tests := []struct {
 		name    string
-		fields  fields
-		args    args
+		fields  []string
+		args    []args
+		want    string
 		wantErr bool
 	}{
-	// TODO: Add test cases.
+		{"add one new to new DB",
+			[]string{},
+			[]args{args{"1", []string{"a@b.c"}}},
+			"1\ta@b.c\n",
+			false},
+		{"add several new to new DB",
+			[]string{},
+			[]args{
+				args{"1", []string{"a@b.c"}},
+				args{"2", []string{"d@e.f", "g@h.i"}},
+				args{"3", []string{"j@k.l"}},
+			},
+			"1\ta@b.c\n2\td@e.f,g@h.i\n3\tj@k.l\n",
+			false},
+		{"add one new to old DB",
+			[]string{
+				"A\tz@y.x",
+				"B\tw@v.u",
+				"C\tt@s.r,q@p.o",
+			},
+			[]args{args{"1", []string{"a@b.c"}}},
+			"A\tz@y.x\nB\tw@v.u\nC\tt@s.r,q@p.o\n1\ta@b.c\n",
+			false},
+		{"add several new to old DB",
+			[]string{
+				"A\tz@y.x",
+				"B\tw@v.u",
+				"C\tt@s.r,q@p.o",
+			},
+			[]args{
+				args{"1", []string{"a@b.c"}},
+				args{"2", []string{"d@e.f", "g@h.i"}},
+				args{"3", []string{"j@k.l"}},
+			},
+			"A\tz@y.x\nB\tw@v.u\nC\tt@s.r,q@p.o\n1\ta@b.c\n2\td@e.f,g@h.i\n3\tj@k.l\n",
+			false},
+		{"change one",
+			[]string{
+				"A\tz@y.x",
+				"1\tw@v.u",
+				"C\tt@s.r,q@p.o",
+			},
+			[]args{args{"1", []string{"a@b.c"}}},
+			"A\tz@y.x\n1\ta@b.c\nC\tt@s.r,q@p.o\n",
+			false},
+		{"change several",
+			[]string{
+				"A\tz@y.x",
+				"3\tw@v.u",
+				"C\tt@s.r,q@p.o",
+				"2\tw@v.u",
+			},
+			[]args{
+				args{"2", []string{"d@e.f", "g@h.i"}},
+				args{"3", []string{"j@k.l"}},
+			},
+			"A\tz@y.x\n3\tj@k.l\nC\tt@s.r,q@p.o\n2\td@e.f,g@h.i\n",
+			false},
+		{"change and add",
+			[]string{
+				"A\tz@y.x",
+				"3\tw@v.u",
+				"C\tt@s.r,q@p.o",
+			},
+			[]args{
+				args{"2", []string{"a@b.c"}},
+				args{"3", []string{"d@e.f"}},
+			},
+			"A\tz@y.x\n3\td@e.f\nC\tt@s.r,q@p.o\n2\ta@b.c\n",
+			false},
 	}
+	dbName := "testdb"
+	os.Remove(dbName)
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			d := db{
-				path: tt.fields.path,
+			// Set up the test DB
+			fout, err := os.Create(dbName)
+			if err != nil {
+				t.Errorf("error setting up db %s", err)
+			}
+			// Clean up test data
+			defer os.Remove(dbName)
+			for _, row := range tt.fields {
+				fout.WriteString(row + "\n")
 			}
-			if err := d.UpdateInvites(tt.args.id, tt.args.as); (err != nil) != tt.wantErr {
-				t.Errorf("db.UpdateInvites() %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
+			fout.Close()
+			d := OpenDb(dbName)
+			for _, inv := range tt.args {
+				if err := d.UpdateInvites(inv.id, inv.as); (err != nil) != tt.wantErr {
+					t.Errorf("db.UpdateInvites() %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
+				}
+			}
+			if got, err := ioutil.ReadFile(dbName); (err != nil) || (string(got) != tt.want) {
+				t.Errorf("db.UpdateInvites() `%s`; error = %v\nwant:\n`%v`\ngot:\n`%v`", tt.name, err, tt.want, string(got))
 			}
 		})
 	}
 }
 
-func Test_db_save(t *testing.T) {
-	type fields struct {
-		path string
-	}
-	type args struct {
-		rs [][]string
-	}
-	tests := []struct {
-		name    string
-		fields  fields
-		args    args
-		wantErr bool
-	}{
-	// TODO: Add test cases.
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			d := db{
-				path: tt.fields.path,
-			}
-			if err := d.save(tt.args.rs); (err != nil) != tt.wantErr {
-				t.Errorf("db.save() %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
-			}
-		})
-	}
-}
-
-func Test_db_readDb(t *testing.T) {
-	type fields struct {
-		path string
-	}
-	tests := []struct {
-		name    string
-		fields  fields
-		want    [][]string
-		wantErr bool
-	}{
-	// TODO: Add test cases.
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			d := db{
-				path: tt.fields.path,
-			}
-			got, err := d.readDb()
-			if (err != nil) != tt.wantErr {
-				t.Errorf("db.readDb() %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("db.readDb() %s = %v, want %v", tt.name, got, tt.want)
-			}
-		})
-	}
-}
+// TODO test locking
+// TODO test malformed DB

          
M main.go +3 -2
@@ 16,6 16,7 @@ var VERSION string = "dev"
 
 // TODO -C 	 	-- send only to changed attendees
 // TODO -e 	 	-- input is an email
+// TODO repair db
 func main() {
 	var send Send
 

          
@@ 26,8 27,8 @@ func main() {
 		return
 	}
 
-	cal := NewCal(send.CalPath)
-	dtb := NewDb(send.DbPath)
+	cal := OpenCal(send.CalPath)
+	dtb := OpenDb(send.DbPath)
 
 	if send.Query {
 		dbs := dtb.Stats()

          
M main_test.go +6 -14
@@ 9,16 9,8 @@ import (
 )
 
 func Test_main(t *testing.T) {
-	tests := []struct {
-		name string
-	}{
-	// TODO: Add test cases.
-	}
-	for range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			main()
-		})
-	}
+	// EMPTY TEST
+	// Never test this, and never generate it (testmd)
 }
 
 func Test_sendInvites(t *testing.T) {

          
@@ 32,7 24,7 @@ func Test_sendInvites(t *testing.T) {
 		args    args
 		wantErr bool
 	}{
-	// TODO: Add test cases.
+		// TODO: Add test cases.
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {

          
@@ 61,11 53,11 @@ func TestSend_Metadata(t *testing.T) {
 		fields fields
 		want   map[string]flag.Flag
 	}{
-	// TODO: Add test cases.
+		// TODO: Add test cases.
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			t := &Send{
+			ts := &Send{
 				One:         tt.fields.One,
 				Version:     tt.fields.Version,
 				Query:       tt.fields.Query,

          
@@ 77,7 69,7 @@ func TestSend_Metadata(t *testing.T) {
 				CalPath:     tt.fields.CalPath,
 				MailInput:   tt.fields.MailInput,
 			}
-			if got := t.Metadata(); !reflect.DeepEqual(got, tt.want) {
+			if got := ts.Metadata(); !reflect.DeepEqual(got, tt.want) {
 				t.Errorf("Send.Metadata() %s = %v, want %v", tt.name, got, tt.want)
 			}
 		})