package tests import "database/sql" import "path/filepath" import "testing" import "time" import "codit/internal/auth" import "codit/internal/db" import "codit/internal/models" import "codit/internal/util" import _ "modernc.org/sqlite" func openTestStore(t *testing.T) *db.Store { var dir string var dsn string var store *db.Store var err error dir = t.TempDir() dsn = "file:" + filepath.Join(dir, "test.db") + "?_pragma=foreign_keys(1)" store, err = db.Open("sqlite", dsn) if err != nil { t.Fatalf("open db: %v", err) } err = store.ApplyMigrations(filepath.Join("..", "migrations")) if err != nil { t.Fatalf("migrate db: %v", err) } return store } func createTestUser(t *testing.T, store *db.Store, username string) models.User { var passwordHash string var err error var user models.User passwordHash, err = auth.HashPassword("pass-123") if err != nil { t.Fatalf("hash password: %v", err) } user = models.User{ Username: username, DisplayName: username, Email: username + "@local", IsAdmin: false, Disabled: false, AuthSource: "db", } user, err = store.CreateUser(user, passwordHash) if err != nil { t.Fatalf("create user: %v", err) } return user } func createAPIKey(t *testing.T, store *db.Store, userID string, name string, expiresAt int64) string { var token string var prefix string var hash string var key models.APIKey var err error token = "tok-" + name + "-" + time.Now().UTC().Format(time.RFC3339Nano) if len(token) > 8 { prefix = token[:8] } else { prefix = token } hash = util.HashToken(token) key, err = store.CreateAPIKey(userID, name, hash, prefix, expiresAt) if err != nil { t.Fatalf("create api key: %v", err) } if key.ID == "" { t.Fatalf("create api key: empty id") } return token } func createTestServicePrincipal(t *testing.T, store *db.Store, name string) models.ServicePrincipal { var principal models.ServicePrincipal var err error principal = models.ServicePrincipal{ Name: name, Description: name, IsAdmin: false, Disabled: false, } principal, err = store.CreateServicePrincipal(principal) if err != nil { t.Fatalf("create service principal: %v", err) } return principal } func createPrincipalAPIKey(t *testing.T, store *db.Store, principalID string, name string, expiresAt int64) string { var token string var prefix string var hash string var key models.PrincipalAPIKey var err error token = "ptok-" + name + "-" + time.Now().UTC().Format(time.RFC3339Nano) if len(token) > 8 { prefix = token[:8] } else { prefix = token } hash = util.HashToken(token) key, err = store.CreatePrincipalAPIKey(principalID, name, hash, prefix, expiresAt) if err != nil { t.Fatalf("create principal api key: %v", err) } if key.ID == "" { t.Fatalf("create principal api key: empty id") } return token } func TestAPIKeyAuthSuccess(t *testing.T) { var store *db.Store var user models.User var token string var got models.User var err error store = openTestStore(t) defer store.Close() user = createTestUser(t, store, "alice") token = createAPIKey(t, store, user.ID, "default", 0) got, err = store.GetUserByAPIKeyHash(util.HashToken(token)) if err != nil { t.Fatalf("lookup by api key: %v", err) } if got.ID != user.ID { t.Fatalf("unexpected user id: got=%s want=%s", got.ID, user.ID) } } func TestAPIKeyAuthExpiredKeyFails(t *testing.T) { var store *db.Store var user models.User var token string var err error store = openTestStore(t) defer store.Close() user = createTestUser(t, store, "bob") token = createAPIKey(t, store, user.ID, "expired", time.Now().UTC().Unix()-60) _, err = store.GetUserByAPIKeyHash(util.HashToken(token)) if err == nil { t.Fatalf("expected error for expired key") } if err != sql.ErrNoRows { t.Fatalf("unexpected error for expired key: %v", err) } } func TestAPIKeyAuthDisabledKeyFails(t *testing.T) { var store *db.Store var user models.User var token string var keys []models.APIKey var keyID string var err error store = openTestStore(t) defer store.Close() user = createTestUser(t, store, "carol") token = createAPIKey(t, store, user.ID, "disabled", 0) keys, err = store.ListAPIKeys(user.ID) if err != nil { t.Fatalf("list api keys: %v", err) } if len(keys) != 1 { t.Fatalf("expected one key, got %d", len(keys)) } keyID = keys[0].ID err = store.SetAPIKeyDisabled(user.ID, keyID, true) if err != nil { t.Fatalf("disable key: %v", err) } _, err = store.GetUserByAPIKeyHash(util.HashToken(token)) if err == nil { t.Fatalf("expected error for disabled key") } if err != sql.ErrNoRows { t.Fatalf("unexpected error for disabled key: %v", err) } } func TestAPIKeyAuthDisabledUserFails(t *testing.T) { var store *db.Store var user models.User var token string var err error store = openTestStore(t) defer store.Close() user = createTestUser(t, store, "dave") token = createAPIKey(t, store, user.ID, "user-disabled", 0) err = store.SetUserDisabled(user.ID, true) if err != nil { t.Fatalf("disable user: %v", err) } _, err = store.GetUserByAPIKeyHash(util.HashToken(token)) if err == nil { t.Fatalf("expected error for disabled user") } if err != sql.ErrNoRows { t.Fatalf("unexpected error for disabled user: %v", err) } } func TestDeleteUserCascadesAPIKeys(t *testing.T) { var store *db.Store var user models.User var keys []models.APIKey var err error store = openTestStore(t) defer store.Close() user = createTestUser(t, store, "erin") _ = createAPIKey(t, store, user.ID, "one", 0) _ = createAPIKey(t, store, user.ID, "two", 0) keys, err = store.ListAPIKeys(user.ID) if err != nil { t.Fatalf("list api keys before delete: %v", err) } if len(keys) != 2 { t.Fatalf("expected two keys before delete, got %d", len(keys)) } err = store.DeleteUser(user.ID) if err != nil { t.Fatalf("delete user: %v", err) } keys, err = store.ListAPIKeys(user.ID) if err != nil { t.Fatalf("list api keys after delete: %v", err) } if len(keys) != 0 { t.Fatalf("expected zero keys after delete, got %d", len(keys)) } } func TestPrincipalAPIKeyAuthSuccess(t *testing.T) { var store *db.Store var principal models.ServicePrincipal var token string var got models.ServicePrincipal var err error store = openTestStore(t) defer store.Close() principal = createTestServicePrincipal(t, store, "robot") token = createPrincipalAPIKey(t, store, principal.ID, "default", 0) got, err = store.GetPrincipalByAPIKeyHash(util.HashToken(token)) if err != nil { t.Fatalf("lookup principal by api key: %v", err) } if got.ID != principal.ID { t.Fatalf("unexpected principal id: got=%s want=%s", got.ID, principal.ID) } } func TestPrincipalAPIKeyAuthDisabledPrincipalFails(t *testing.T) { var store *db.Store var principal models.ServicePrincipal var token string var err error store = openTestStore(t) defer store.Close() principal = createTestServicePrincipal(t, store, "robot-disabled") token = createPrincipalAPIKey(t, store, principal.ID, "default", 0) principal.Disabled = true err = store.UpdateServicePrincipal(principal) if err != nil { t.Fatalf("disable principal: %v", err) } _, err = store.GetPrincipalByAPIKeyHash(util.HashToken(token)) if err == nil { t.Fatalf("expected error for disabled principal") } if err != sql.ErrNoRows { t.Fatalf("unexpected error for disabled principal: %v", err) } } func TestListAPIKeysAdminIncludesPrincipalKeys(t *testing.T) { var store *db.Store var user models.User var principal models.ServicePrincipal var items []models.AdminAPIKey var err error store = openTestStore(t) defer store.Close() user = createTestUser(t, store, "frank") principal = createTestServicePrincipal(t, store, "robot-admin-list") _ = createAPIKey(t, store, user.ID, "user-key", 0) _ = createPrincipalAPIKey(t, store, principal.ID, "principal-key", 0) items, err = store.ListAPIKeysAdmin("", "") if err != nil { t.Fatalf("list admin api keys: %v", err) } if len(items) != 2 { t.Fatalf("expected two admin api keys, got %d", len(items)) } if items[0].SubjectType == items[1].SubjectType { t.Fatalf("expected mixed subject types, got %q and %q", items[0].SubjectType, items[1].SubjectType) } }