init
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCreateAndReadProject(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := testDB(t)
|
||||
|
||||
p := DefaultProject("demo")
|
||||
p.DisplayName = "Demo Site"
|
||||
p.NotFoundFile = "404.html"
|
||||
p.SPAFallback = true
|
||||
if err := db.CreateProject(ctx, p); err != nil {
|
||||
t.Fatalf("CreateProject: %v", err)
|
||||
}
|
||||
if p.ID == 0 {
|
||||
t.Error("CreateProject must fill in the ID")
|
||||
}
|
||||
if p.CreatedAt.IsZero() || !p.UpdatedAt.Equal(p.CreatedAt) {
|
||||
t.Errorf("timestamps not set: created=%v updated=%v", p.CreatedAt, p.UpdatedAt)
|
||||
}
|
||||
|
||||
got, err := db.ProjectByName(ctx, "demo")
|
||||
if err != nil {
|
||||
t.Fatalf("ProjectByName: %v", err)
|
||||
}
|
||||
if got.ID != p.ID || got.DisplayName != "Demo Site" || got.NotFoundFile != "404.html" || !got.SPAFallback {
|
||||
t.Errorf("round trip lost data: %+v", got)
|
||||
}
|
||||
if got.IndexFile != "index.html" || got.RetentionCount != 10 {
|
||||
t.Errorf("defaults not persisted: %+v", got)
|
||||
}
|
||||
|
||||
byID, err := db.ProjectByID(ctx, p.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ProjectByID: %v", err)
|
||||
}
|
||||
if byID.Name != "demo" {
|
||||
t.Errorf("ProjectByID returned %q", byID.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// An empty not_found_file must come back as "" rather than as a bogus "NULL"
|
||||
// string, because the resolver branches on it being empty.
|
||||
func TestProjectNullNotFoundFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := testDB(t)
|
||||
if err := db.CreateProject(ctx, DefaultProject("demo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := db.ProjectByName(ctx, "demo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.NotFoundFile != "" {
|
||||
t.Errorf("NotFoundFile = %q, want empty", got.NotFoundFile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProjectDuplicateName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := testDB(t)
|
||||
if err := db.CreateProject(ctx, DefaultProject("demo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := db.CreateProject(ctx, DefaultProject("demo"))
|
||||
if !errors.Is(err, ErrExists) {
|
||||
t.Fatalf("second create: got %v, want ErrExists", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectNotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := testDB(t)
|
||||
if _, err := db.ProjectByName(ctx, "nope"); !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("ProjectByName: got %v, want ErrNotFound", err)
|
||||
}
|
||||
if _, err := db.ProjectByID(ctx, 404); !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("ProjectByID: got %v, want ErrNotFound", err)
|
||||
}
|
||||
if err := db.DeleteProject(ctx, 404); !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("DeleteProject: got %v, want ErrNotFound", err)
|
||||
}
|
||||
if err := db.UpdateProject(ctx, &Project{ID: 404}); !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("UpdateProject: got %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateProject(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := testDB(t)
|
||||
p := DefaultProject("demo")
|
||||
if err := db.CreateProject(ctx, p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.SPAFallback = true
|
||||
p.CacheControl = "public, max-age=31536000, immutable"
|
||||
p.RetentionCount = 3
|
||||
p.NotFoundFile = "404.html"
|
||||
if err := db.UpdateProject(ctx, p); err != nil {
|
||||
t.Fatalf("UpdateProject: %v", err)
|
||||
}
|
||||
|
||||
got, err := db.ProjectByName(ctx, "demo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !got.SPAFallback || got.RetentionCount != 3 || got.NotFoundFile != "404.html" {
|
||||
t.Errorf("update did not stick: %+v", got)
|
||||
}
|
||||
if got.Name != "demo" {
|
||||
t.Errorf("name must be immutable, got %q", got.Name)
|
||||
}
|
||||
if got.CreatedAt.After(got.UpdatedAt) {
|
||||
t.Errorf("updated_at %v predates created_at %v", got.UpdatedAt, got.CreatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
// Clearing not_found_file must write SQL NULL, not the empty string, so the
|
||||
// column keeps a single representation of "unset".
|
||||
func TestUpdateProjectClearsNotFoundFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := testDB(t)
|
||||
p := DefaultProject("demo")
|
||||
p.NotFoundFile = "404.html"
|
||||
if err := db.CreateProject(ctx, p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.NotFoundFile = ""
|
||||
if err := db.UpdateProject(ctx, p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var isNull bool
|
||||
if err := db.Reader().QueryRow(
|
||||
`SELECT not_found_file IS NULL FROM projects WHERE id = ?`, p.ID).Scan(&isNull); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !isNull {
|
||||
t.Error("cleared not_found_file should be stored as NULL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListProjectsPaging(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := testDB(t)
|
||||
for i := 0; i < 7; i++ {
|
||||
if err := db.CreateProject(ctx, DefaultProject(fmt.Sprintf("p%d", i))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var names []string
|
||||
cursor := ""
|
||||
for pages := 0; ; pages++ {
|
||||
if pages > 10 {
|
||||
t.Fatal("paging did not terminate")
|
||||
}
|
||||
batch, next, err := db.ListProjects(ctx, 3, cursor)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range batch {
|
||||
names = append(names, p.Name)
|
||||
}
|
||||
if next == "" {
|
||||
break
|
||||
}
|
||||
cursor = next
|
||||
}
|
||||
want := []string{"p0", "p1", "p2", "p3", "p4", "p5", "p6"}
|
||||
if len(names) != len(want) {
|
||||
t.Fatalf("paged names = %v, want %v", names, want)
|
||||
}
|
||||
for i := range want {
|
||||
if names[i] != want[i] {
|
||||
t.Fatalf("paged names = %v, want %v", names, want)
|
||||
}
|
||||
}
|
||||
|
||||
n, err := db.CountProjects(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 7 {
|
||||
t.Errorf("CountProjects = %d, want 7", n)
|
||||
}
|
||||
|
||||
all, err := db.AllProjects(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(all) != 7 {
|
||||
t.Errorf("AllProjects returned %d rows, want 7", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// Deleting a project must take its keys, deployments and manifest rows with it,
|
||||
// and must drop the blob refcounts so the content becomes collectable.
|
||||
func TestDeleteProjectCascades(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := testDB(t)
|
||||
p := DefaultProject("demo")
|
||||
if err := db.CreateProject(ctx, p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := make([]byte, 32)
|
||||
digest[0] = 0x7f
|
||||
|
||||
if err := db.Tx(ctx, func(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(`INSERT INTO deployments (id, public_id, project_id, state, created_at)
|
||||
VALUES (1, 'dpl_a', ?, 'ready', 1)`, p.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(`INSERT INTO blobs (digest, size, present, created_at, last_ref_at)
|
||||
VALUES (?, 5, 1, 1, 1)`, digest); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tx.Exec(`INSERT INTO deployment_files (deployment_id, path, digest, size)
|
||||
VALUES (1, 'index.html', ?, 5)`, digest)
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pid := p.ID
|
||||
if err := db.CreateKey(ctx, &APIKey{
|
||||
ID: "keyaaaaaaaaaaaaa", SecretHash: make([]byte, 32), Scope: ScopeProject, ProjectID: &pid,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := db.DeleteProject(ctx, p.ID); err != nil {
|
||||
t.Fatalf("DeleteProject: %v", err)
|
||||
}
|
||||
|
||||
count := func(query string, args ...any) int {
|
||||
t.Helper()
|
||||
var n int
|
||||
if err := db.Reader().QueryRow(query, args...).Scan(&n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
if n := count(`SELECT count(*) FROM deployments`); n != 0 {
|
||||
t.Errorf("%d deployments survived", n)
|
||||
}
|
||||
if n := count(`SELECT count(*) FROM deployment_files`); n != 0 {
|
||||
t.Errorf("%d manifest rows survived", n)
|
||||
}
|
||||
if n := count(`SELECT count(*) FROM api_keys`); n != 0 {
|
||||
t.Errorf("%d keys survived", n)
|
||||
}
|
||||
if n := count(`SELECT refcount FROM blobs WHERE digest = ?`, digest); n != 0 {
|
||||
t.Errorf("blob refcount = %d, want 0 (content would never be collected)", n)
|
||||
}
|
||||
// The blob row itself stays: it is now unreferenced, and reclaiming it is
|
||||
// GC's job, not the delete path's.
|
||||
if n := count(`SELECT count(*) FROM blobs`); n != 1 {
|
||||
t.Errorf("blob row count = %d, want 1", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectTimestampsAreUTC(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := testDB(t)
|
||||
p := DefaultProject("demo")
|
||||
if err := db.CreateProject(ctx, p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := db.ProjectByName(ctx, "demo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.CreatedAt.Location() != time.UTC {
|
||||
t.Errorf("CreatedAt location = %v, want UTC", got.CreatedAt.Location())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user