package main import ( "context" "testing" ) func (a *app) seedSong(t *testing.T, submitter int64, title string) int64 { t.Helper() var id int64 err := a.db.QueryRowContext(context.Background(), ` insert into songs (title, artist, genre, audio_file, duration_seconds, submitted_by) values ($1, 'Testiartisti', 'Metal', 'x.ogg', 120, $2) returning id`, title, submitter).Scan(&id) if err != nil { t.Fatal(err) } return id } func (a *app) seedReview(t *testing.T, songID, reviewerID int64, score int) int64 { t.Helper() var id int64 err := a.db.QueryRowContext(context.Background(), ` insert into reviews (song_id, reviewer_id, score, text) values ($1, $2, $3, 'sanat') returning id`, songID, reviewerID, score).Scan(&id) if err != nil { t.Fatal(err) } return id } // A member who hasn't reviewed a song must not receive other reviews *in the result set*, and must // not receive the average either — not merely fail to render them. func TestRevealRuleWithholdsReviewsAndAverage(t *testing.T) { a := testApp(t) ctx := context.Background() aino := a.seedMember(t, "aino@example.com") bertta := a.seedMember(t, "bertta@example.com") cecilia := a.seedMember(t, "cecilia@example.com") songID := a.seedSong(t, aino, "Testikappale") a.seedReview(t, songID, bertta, 88) // Cecilia has not reviewed it. d, err := a.song(ctx, cecilia, songID) if err != nil { t.Fatal(err) } if d.Revealed() { t.Fatal("song is revealed to a member who has not reviewed it") } if d.Reviews != nil { t.Fatalf("withheld reviews were still fetched: %d of them", len(d.Reviews)) } if d.Average != nil { t.Fatalf("withheld average was still sent: %v", *d.Average) } if d.ReviewCount != 1 { t.Fatalf("review count = %d, want 1 — the count is not secret", d.ReviewCount) } // Writing her own review unlocks both. a.seedReview(t, songID, cecilia, 60) d, err = a.song(ctx, cecilia, songID) if err != nil { t.Fatal(err) } if !d.Revealed() || len(d.Reviews) != 2 { t.Fatalf("after reviewing: revealed = %v, reviews = %d, want true and 2", d.Revealed(), len(d.Reviews)) } if d.Average == nil || *d.Average != 74 { t.Fatalf("average = %v, want 74", d.Average) } // The submitter sees everything without reviewing — they cannot review their own song. d, err = a.song(ctx, aino, songID) if err != nil { t.Fatal(err) } if !d.Revealed() || len(d.Reviews) != 2 || d.Average == nil { t.Fatal("the submitter cannot see the reviews of their own song") } if d.CanReview { t.Fatal("the submitter is offered a review form for their own song") } // And the same rule holds in the list query, which is a different SQL path. list, err := a.browse(ctx, cecilia, 0) if err != nil { t.Fatal(err) } if len(list.Items) != 1 || list.Items[0].Average == nil { t.Fatal("browse withheld the average from someone who has reviewed the song") } list, err = a.browse(ctx, a.seedMember(t, "dora@example.com"), 0) if err != nil { t.Fatal(err) } if list.Items[0].Average != nil { t.Fatal("browse leaked the average to someone who has not reviewed the song") } } // The queue excludes your own songs and anything you have already reviewed, oldest first. func TestQueueContents(t *testing.T) { a := testApp(t) ctx := context.Background() aino := a.seedMember(t, "aino@example.com") bertta := a.seedMember(t, "bertta@example.com") own := a.seedSong(t, aino, "Oma kappale") reviewed := a.seedSong(t, bertta, "Jo arvosteltu") fresh := a.seedSong(t, bertta, "Arvostelematon") a.seedReview(t, reviewed, aino, 50) list, err := a.queue(ctx, aino, 0) if err != nil { t.Fatal(err) } if len(list.Items) != 1 { var titles []string for _, s := range list.Items { titles = append(titles, s.Title) } t.Fatalf("queue = %v, want just the unreviewed song", titles) } if list.Items[0].ID != fresh { t.Fatalf("queue holds song %d, want %d", list.Items[0].ID, fresh) } _ = own // Oldest first: a second unreviewed song comes after the first. older := a.seedSong(t, bertta, "Vanhempi") if _, err := a.db.ExecContext(ctx, `update songs set created_at = datetime('now', '-2 days') where id = $1`, older); err != nil { t.Fatal(err) } list, err = a.queue(ctx, aino, 0) if err != nil { t.Fatal(err) } if list.Items[0].ID != older { t.Fatal("queue is not oldest first") } } // Locked is a live state: deleting the only review makes the song editable again. func TestSongUnlocksWhenTheLastReviewGoes(t *testing.T) { a := testApp(t) ctx := context.Background() aino := a.seedMember(t, "aino@example.com") bertta := a.seedMember(t, "bertta@example.com") songID := a.seedSong(t, aino, "Testikappale") d, _ := a.song(ctx, aino, songID) if !d.CanEdit { t.Fatal("a song with no reviews is not editable by its submitter") } reviewID := a.seedReview(t, songID, bertta, 88) d, _ = a.song(ctx, aino, songID) if d.CanEdit { t.Fatal("a reviewed song is still editable") } if _, err := a.db.ExecContext(ctx, `delete from reviews where id = $1`, reviewID); err != nil { t.Fatal(err) } d, _ = a.song(ctx, aino, songID) if !d.CanEdit { t.Fatal("song did not unlock after its only review was deleted") } } // The window is measured from updated_at, so an edit extends it — and it gates delete too. func TestEditWindow(t *testing.T) { a := testApp(t) ctx := context.Background() aino := a.seedMember(t, "aino@example.com") bertta := a.seedMember(t, "bertta@example.com") songID := a.seedSong(t, aino, "Testikappale") reviewID := a.seedReview(t, songID, bertta, 88) v, err := a.viewerReview(ctx, songID, bertta) if err != nil { t.Fatal(err) } if !v.CanEdit() { t.Fatal("a fresh review is not editable") } // Just inside the window. if _, err := a.db.ExecContext(ctx, `update reviews set updated_at = datetime('now', '-29 minutes') where id = $1`, reviewID); err != nil { t.Fatal(err) } v, _ = a.viewerReview(ctx, songID, bertta) if !v.CanEdit() { t.Fatal("a 29-minute-old review is not editable") } // Past it. if _, err := a.db.ExecContext(ctx, `update reviews set updated_at = datetime('now', '-31 minutes') where id = $1`, reviewID); err != nil { t.Fatal(err) } v, _ = a.viewerReview(ctx, songID, bertta) if v.CanEdit() { t.Fatal("a 31-minute-old review is still editable") } // The database is the authority, not the Go clock: the update and the delete both refuse. var n int64 err = a.db.QueryRowContext(ctx, ` update reviews set score = 1, updated_at = datetime('now') where id = $1 and reviewer_id = $2 and updated_at > datetime('now', $3) returning id`, reviewID, bertta, editWindowAgo).Scan(&n) if err == nil { t.Fatal("an expired review was edited") } err = a.db.QueryRowContext(ctx, ` delete from reviews where id = $1 and reviewer_id = $2 and updated_at > datetime('now', $3) returning id`, reviewID, bertta, editWindowAgo).Scan(&n) if err == nil { t.Fatal("an expired review was deleted") } }