josie / simplegit

package db

import (
	"errors"
	"testing"
)

func TestCreateIssueNumbersPerRepo(t *testing.T) {
	database := openTestDB(t)
	ownerID, _ := CreateUser(database, "josie", "hash")
	repoA, err := CreateRepo(database, ownerID, "alpha", "", "public")
	if err != nil {
		t.Fatalf("CreateRepo A: %v", err)
	}
	repoB, err := CreateRepo(database, ownerID, "beta", "", "public")
	if err != nil {
		t.Fatalf("CreateRepo B: %v", err)
	}

	first, err := CreateIssue(database, repoA.ID, "one", "body", ownerID, "josie", "", false)
	if err != nil {
		t.Fatalf("CreateIssue A1: %v", err)
	}
	second, err := CreateIssue(database, repoA.ID, "two", "body", ownerID, "josie", "", false)
	if err != nil {
		t.Fatalf("CreateIssue A2: %v", err)
	}
	other, err := CreateIssue(database, repoB.ID, "other", "body", 0, "guest", "g@example.com", true)
	if err != nil {
		t.Fatalf("CreateIssue B1: %v", err)
	}

	if first.Number != 1 || second.Number != 2 {
		t.Errorf("repo A numbers = %d, %d, want 1, 2", first.Number, second.Number)
	}
	if other.Number != 1 {
		t.Errorf("repo B number = %d, want 1 (independent)", other.Number)
	}
	if other.Pending != true {
		t.Error("guest issue Pending = false, want true")
	}
	if other.AuthorID.Valid {
		t.Error("guest issue AuthorID is set, want NULL")
	}
	if other.AuthorName != "guest" || other.AuthorEmail != "g@example.com" {
		t.Errorf("guest author = %q/%q, want guest/g@example.com", other.AuthorName, other.AuthorEmail)
	}
	if !first.AuthorID.Valid || first.AuthorID.Int64 != ownerID {
		t.Errorf("owner issue AuthorID = %+v, want %d", first.AuthorID, ownerID)
	}
}

func TestGetIssueByNumberIsRepoScoped(t *testing.T) {
	database := openTestDB(t)
	ownerID, _ := CreateUser(database, "josie", "hash")
	repoA, _ := CreateRepo(database, ownerID, "alpha", "", "public")
	repoB, _ := CreateRepo(database, ownerID, "beta", "", "public")
	if _, err := CreateIssue(database, repoA.ID, "a1", "", ownerID, "josie", "", false); err != nil {
		t.Fatalf("CreateIssue: %v", err)
	}

	if _, err := GetIssueByNumber(database, repoB.ID, 1); !errors.Is(err, ErrNotFound) {
		t.Errorf("GetIssueByNumber(other repo, 1) err = %v, want ErrNotFound", err)
	}
}

func TestListIssuesPendingAndState(t *testing.T) {
	database := openTestDB(t)
	ownerID, _ := CreateUser(database, "josie", "hash")
	repo, _ := CreateRepo(database, ownerID, "alpha", "", "public")

	published, _ := CreateIssue(database, repo.ID, "published", "", ownerID, "josie", "", false)
	pending, _ := CreateIssue(database, repo.ID, "pending", "", 0, "guest", "", true)

	public, err := ListIssues(database, repo.ID, false, "")
	if err != nil {
		t.Fatalf("ListIssues public: %v", err)
	}
	if len(public) != 1 || public[0].Title != "published" {
		t.Errorf("public list = %+v, want only the published issue", public)
	}

	owner, err := ListIssues(database, repo.ID, true, "")
	if err != nil {
		t.Fatalf("ListIssues owner: %v", err)
	}
	if len(owner) != 2 || !owner[0].Pending {
		t.Errorf("owner list = %+v, want 2 with pending first", owner)
	}

	if err := SetIssueState(database, repo.ID, published.Number, "closed"); err != nil {
		t.Fatalf("SetIssueState closed: %v", err)
	}
	closed, err := ListIssues(database, repo.ID, true, "closed")
	if err != nil {
		t.Fatalf("ListIssues closed: %v", err)
	}
	if len(closed) != 1 || closed[0].Number != published.Number {
		t.Errorf("closed list = %+v, want the closed issue", closed)
	}
	open, err := ListIssues(database, repo.ID, true, "open")
	if err != nil {
		t.Fatalf("ListIssues open: %v", err)
	}
	if len(open) != 1 || open[0].Number != pending.Number {
		t.Errorf("open list = %+v, want the pending issue", open)
	}

	reloaded, err := GetIssueByNumber(database, repo.ID, published.Number)
	if err != nil {
		t.Fatalf("GetIssueByNumber: %v", err)
	}
	if !reloaded.ClosedAt.Valid {
		t.Error("ClosedAt is NULL after close")
	}
	if err := SetIssueState(database, repo.ID, published.Number, "open"); err != nil {
		t.Fatalf("SetIssueState reopen: %v", err)
	}
	reopened, _ := GetIssueByNumber(database, repo.ID, published.Number)
	if reopened.ClosedAt.Valid {
		t.Error("ClosedAt is set after reopen, want NULL")
	}
}

func TestApproveAndDeleteIssue(t *testing.T) {
	database := openTestDB(t)
	ownerID, _ := CreateUser(database, "josie", "hash")
	repo, _ := CreateRepo(database, ownerID, "alpha", "", "public")
	issue, _ := CreateIssue(database, repo.ID, "spam?", "", 0, "guest", "", true)

	if err := ApproveIssue(database, repo.ID, issue.Number); err != nil {
		t.Fatalf("ApproveIssue: %v", err)
	}
	got, _ := GetIssueByNumber(database, repo.ID, issue.Number)
	if got.Pending {
		t.Error("issue still pending after approve")
	}

	if err := DeleteIssue(database, repo.ID, issue.Number); err != nil {
		t.Fatalf("DeleteIssue: %v", err)
	}
	if _, err := GetIssueByNumber(database, repo.ID, issue.Number); !errors.Is(err, ErrNotFound) {
		t.Errorf("GetIssueByNumber after delete err = %v, want ErrNotFound", err)
	}
}

func TestCommentsModerationAndCount(t *testing.T) {
	database := openTestDB(t)
	ownerID, _ := CreateUser(database, "josie", "hash")
	repo, _ := CreateRepo(database, ownerID, "alpha", "", "public")
	issue, _ := CreateIssue(database, repo.ID, "topic", "", ownerID, "josie", "", false)

	ownerComment, _ := CreateComment(database, issue.ID, "owner reply", ownerID, "josie", "", false)
	guestComment, _ := CreateComment(database, issue.ID, "guest reply", 0, "guest", "guest@example.com", true)

	public, err := ListComments(database, issue.ID, false)
	if err != nil {
		t.Fatalf("ListComments public: %v", err)
	}
	if len(public) != 1 || public[0].ID != ownerComment.ID {
		t.Errorf("public comments = %+v, want only the owner comment", public)
	}

	owner, err := ListComments(database, issue.ID, true)
	if err != nil {
		t.Fatalf("ListComments owner: %v", err)
	}
	if len(owner) != 2 {
		t.Errorf("owner comments = %d, want 2", len(owner))
	}

	pending, err := CountPending(database, repo.ID)
	if err != nil {
		t.Fatalf("CountPending: %v", err)
	}
	if pending != 1 {
		t.Errorf("CountPending = %d, want 1", pending)
	}

	if err := ApproveComment(database, issue.ID, guestComment.ID); err != nil {
		t.Fatalf("ApproveComment: %v", err)
	}
	if pending, _ = CountPending(database, repo.ID); pending != 0 {
		t.Errorf("CountPending after approve = %d, want 0", pending)
	}

	if err := DeleteComment(database, issue.ID, guestComment.ID); err != nil {
		t.Fatalf("DeleteComment: %v", err)
	}
	if _, err := getComment(database, issueCommentColumns+`id = ?`, guestComment.ID); !errors.Is(err, ErrNotFound) {
		t.Errorf("comment after delete err = %v, want ErrNotFound", err)
	}

	if err := ApproveComment(database, issue.ID+999, ownerComment.ID); !errors.Is(err, ErrNotFound) {
		t.Errorf("ApproveComment wrong issue err = %v, want ErrNotFound", err)
	}
}

func TestDeleteIssueCascadesComments(t *testing.T) {
	database := openTestDB(t)
	ownerID, _ := CreateUser(database, "josie", "hash")
	repo, _ := CreateRepo(database, ownerID, "alpha", "", "public")
	issue, _ := CreateIssue(database, repo.ID, "topic", "", ownerID, "josie", "", false)
	if _, err := CreateComment(database, issue.ID, "reply", ownerID, "josie", "", false); err != nil {
		t.Fatalf("CreateComment: %v", err)
	}

	if err := DeleteIssue(database, repo.ID, issue.Number); err != nil {
		t.Fatalf("DeleteIssue: %v", err)
	}
	comments, err := ListComments(database, issue.ID, true)
	if err != nil {
		t.Fatalf("ListComments: %v", err)
	}
	if len(comments) != 0 {
		t.Errorf("comments after issue delete = %d, want 0 (cascade)", len(comments))
	}
}