init
This commit is contained in:
commit
b15b95781c
108 changed files with 14802 additions and 0 deletions
294
internal/db/migrate_test.go
Normal file
294
internal/db/migrate_test.go
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
package db_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"update_server/internal/db"
|
||||
)
|
||||
|
||||
func TestMigrateAppliesCoreSchema(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
sqlitePath := filepath.Join(t.TempDir(), "update-server.sqlite")
|
||||
|
||||
database, err := db.Open(ctx, sqlitePath)
|
||||
if err != nil {
|
||||
t.Fatalf("open database: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
migrationsDir := projectMigrationsDir(t)
|
||||
if err := db.Migrate(ctx, database, migrationsDir); err != nil {
|
||||
t.Fatalf("apply migrations: %v", err)
|
||||
}
|
||||
|
||||
if err := db.Migrate(ctx, database, migrationsDir); err != nil {
|
||||
t.Fatalf("reapply migrations: %v", err)
|
||||
}
|
||||
|
||||
expectedTables := []string{
|
||||
"schema_migrations",
|
||||
"users",
|
||||
"projects",
|
||||
"tags",
|
||||
"project_tags",
|
||||
"releases",
|
||||
"api_keys",
|
||||
"api_key_project_access",
|
||||
"api_key_tag_access",
|
||||
"sessions",
|
||||
"audit_logs",
|
||||
}
|
||||
|
||||
for _, tableName := range expectedTables {
|
||||
if !tableExists(t, database, tableName) {
|
||||
t.Fatalf("expected table %q to exist", tableName)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO users (email, password_hash, role) VALUES (?, ?, ?)`,
|
||||
"admin@example.com",
|
||||
"hashed-password",
|
||||
"admin",
|
||||
); err != nil {
|
||||
t.Fatalf("insert user: %v", err)
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO projects (name, slug, description) VALUES (?, ?, ?)`,
|
||||
"Desktop App",
|
||||
"desktop-app",
|
||||
"Primary desktop client",
|
||||
); err != nil {
|
||||
t.Fatalf("insert project: %v", err)
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO tags (name, slug, description) VALUES (?, ?, ?)`,
|
||||
"Windows",
|
||||
"windows",
|
||||
"Windows releases",
|
||||
); err != nil {
|
||||
t.Fatalf("insert tag: %v", err)
|
||||
}
|
||||
|
||||
var projectID int64
|
||||
if err := database.QueryRowContext(ctx, `SELECT id FROM projects WHERE slug = ?`, "desktop-app").Scan(&projectID); err != nil {
|
||||
t.Fatalf("load project id: %v", err)
|
||||
}
|
||||
|
||||
var tagID int64
|
||||
if err := database.QueryRowContext(ctx, `SELECT id FROM tags WHERE slug = ?`, "windows").Scan(&tagID); err != nil {
|
||||
t.Fatalf("load tag id: %v", err)
|
||||
}
|
||||
|
||||
var userID int64
|
||||
if err := database.QueryRowContext(ctx, `SELECT id FROM users WHERE email = ?`, "admin@example.com").Scan(&userID); err != nil {
|
||||
t.Fatalf("load user id: %v", err)
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO project_tags (project_id, tag_id) VALUES (?, ?)`,
|
||||
projectID,
|
||||
tagID,
|
||||
); err != nil {
|
||||
t.Fatalf("insert project tag: %v", err)
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO releases (project_id, version, build, filename, storage_path, checksum_sha256, size_bytes, content_type, release_notes, uploaded_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
projectID,
|
||||
"1.0.0",
|
||||
"",
|
||||
"desktop-app-1.0.0.zip",
|
||||
"artifacts/desktop-app/1.0.0/desktop-app-1.0.0.zip",
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
1024,
|
||||
"application/zip",
|
||||
"Initial release",
|
||||
userID,
|
||||
); err != nil {
|
||||
t.Fatalf("insert release: %v", err)
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO api_keys (name, key_prefix, key_hash, description, scope_mode, can_download, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
"Desktop Clients",
|
||||
"updsrv_project",
|
||||
"hash-project",
|
||||
"Project-scoped desktop client access",
|
||||
"project_allow_list",
|
||||
1,
|
||||
userID,
|
||||
); err != nil {
|
||||
t.Fatalf("insert project api key: %v", err)
|
||||
}
|
||||
|
||||
var projectAPIKeyID int64
|
||||
if err := database.QueryRowContext(ctx, `SELECT id FROM api_keys WHERE key_prefix = ?`, "updsrv_project").Scan(&projectAPIKeyID); err != nil {
|
||||
t.Fatalf("load project api key id: %v", err)
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO api_key_project_access (api_key_id, project_id) VALUES (?, ?)`,
|
||||
projectAPIKeyID,
|
||||
projectID,
|
||||
); err != nil {
|
||||
t.Fatalf("insert api key project access: %v", err)
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO api_keys (name, key_prefix, key_hash, description, scope_mode, can_download, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
"Tagged Clients",
|
||||
"updsrv_tag",
|
||||
"hash-tag",
|
||||
"Tag-scoped desktop client access",
|
||||
"tag_allow_list",
|
||||
1,
|
||||
userID,
|
||||
); err != nil {
|
||||
t.Fatalf("insert tag api key: %v", err)
|
||||
}
|
||||
|
||||
var tagAPIKeyID int64
|
||||
if err := database.QueryRowContext(ctx, `SELECT id FROM api_keys WHERE key_prefix = ?`, "updsrv_tag").Scan(&tagAPIKeyID); err != nil {
|
||||
t.Fatalf("load tag api key id: %v", err)
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO api_key_tag_access (api_key_id, tag_id) VALUES (?, ?)`,
|
||||
tagAPIKeyID,
|
||||
tagID,
|
||||
); err != nil {
|
||||
t.Fatalf("insert api key tag access: %v", err)
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO sessions (user_id, token_hash, expires_at, ip_address, user_agent) VALUES (?, ?, ?, ?, ?)`,
|
||||
userID,
|
||||
"session-hash",
|
||||
"2030-01-01T00:00:00Z",
|
||||
"127.0.0.1",
|
||||
"test-agent",
|
||||
); err != nil {
|
||||
t.Fatalf("insert session: %v", err)
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO audit_logs (actor_user_id, api_key_id, action, target_type, target_id, target_identifier, metadata_json, ip_address) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
userID,
|
||||
tagAPIKeyID,
|
||||
"api_key.created",
|
||||
"api_key",
|
||||
tagAPIKeyID,
|
||||
"updsrv_tag",
|
||||
`{"source":"test"}`,
|
||||
"127.0.0.1",
|
||||
); err != nil {
|
||||
t.Fatalf("insert audit log: %v", err)
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO api_key_project_access (api_key_id, project_id) VALUES (?, ?)`,
|
||||
tagAPIKeyID,
|
||||
projectID,
|
||||
); err == nil {
|
||||
t.Fatal("expected project access insert for tag-scoped key to fail")
|
||||
}
|
||||
|
||||
if _, err := database.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO api_keys (name, key_prefix, key_hash, scope_mode) VALUES (?, ?, ?, ?)`,
|
||||
"Broken Key",
|
||||
"updsrv_invalid",
|
||||
"hash-invalid",
|
||||
"invalid_scope",
|
||||
); err == nil {
|
||||
t.Fatal("expected invalid scope_mode insert to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func projectMigrationsDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
_, filename, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("resolve caller path")
|
||||
}
|
||||
|
||||
return filepath.Join(filepath.Dir(filename), "..", "..", "migrations")
|
||||
}
|
||||
|
||||
func tableExists(t *testing.T, database *sql.DB, tableName string) bool {
|
||||
t.Helper()
|
||||
|
||||
var exists int
|
||||
query := `SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?)`
|
||||
if err := database.QueryRowContext(context.Background(), query, tableName).Scan(&exists); err != nil {
|
||||
t.Fatalf("check table %s: %v", tableName, err)
|
||||
}
|
||||
|
||||
return exists == 1
|
||||
}
|
||||
|
||||
func TestMigrateRejectsEditedAppliedMigrations(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
tempDir := t.TempDir()
|
||||
sqlitePath := filepath.Join(tempDir, "update-server.sqlite")
|
||||
|
||||
database, err := db.Open(ctx, sqlitePath)
|
||||
if err != nil {
|
||||
t.Fatalf("open database: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
migrationsDir := filepath.Join(tempDir, "migrations")
|
||||
if err := os.MkdirAll(migrationsDir, 0o755); err != nil {
|
||||
t.Fatalf("create migrations dir: %v", err)
|
||||
}
|
||||
|
||||
firstMigrationPath := filepath.Join(migrationsDir, "0001_test.sql")
|
||||
if err := os.WriteFile(firstMigrationPath, []byte(`CREATE TABLE sample (id INTEGER PRIMARY KEY);`), 0o644); err != nil {
|
||||
t.Fatalf("write migration: %v", err)
|
||||
}
|
||||
|
||||
if err := db.Migrate(ctx, database, migrationsDir); err != nil {
|
||||
t.Fatalf("apply migration: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(firstMigrationPath, []byte(`CREATE TABLE sample (id INTEGER PRIMARY KEY, name TEXT);`), 0o644); err != nil {
|
||||
t.Fatalf("rewrite migration: %v", err)
|
||||
}
|
||||
|
||||
err = db.Migrate(ctx, database, migrationsDir)
|
||||
if err == nil {
|
||||
t.Fatal("expected checksum mismatch error")
|
||||
}
|
||||
|
||||
expectedMessage := "checksum mismatch"
|
||||
if !strings.Contains(err.Error(), expectedMessage) {
|
||||
t.Fatalf("expected error containing %q, got %v", expectedMessage, err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue