diff --git a/backend/internal/service/e2etest_service.go b/backend/internal/service/e2etest_service.go index 21572adc..0d25f85a 100644 --- a/backend/internal/service/e2etest_service.go +++ b/backend/internal/service/e2etest_service.go @@ -282,6 +282,61 @@ func (s *TestService) SeedDatabase(baseURL string) error { } } + farFuture := datatype.DateTime(time.Date(2099, 1, 1, 0, 0, 0, 0, time.UTC)) + oauth2Session := oidc.OAuth2Session{ + Base: model.Base{ + ID: "551ab785-c830-47d3-8a07-60c9f3bb4859", + }, + Kind: "access_token", + Key: "cross-database-test-session", + RequestID: "cross-database-test-request", + AccessTokenSignature: "", + Active: true, + RequestData: `{"request":"value"}`, + ExpiresAt: &farFuture, + } + if err := tx.Create(&oauth2Session).Error; err != nil { + return err + } + + if err := tx.Table("oauth2_jtis").Create(map[string]any{ + "id": "bd0c8bf2-66ec-487a-9dd5-7d9d78d73543", + "created_at": datatype.DateTime(time.Now()), + "jti": "cross-database-test-jti", + "expires_at": farFuture, + }).Error; err != nil { + return err + } + + interactionSession := oidc.InteractionSession{ + Base: model.Base{ + ID: "aaf5dd23-cd1f-4748-a2aa-baa6af94d800", + }, + Scopes: datatype.StringList{"openid"}, + ClientID: oidcClients[0].ID, + UserID: new(users[0].ID), + ConsentRequired: true, + RequestedAt: farFuture, + Parameters: oidc.InteractionSessionParameters{ + "client_id": oidcClients[0].ID, + }, + } + if err := tx.Create(&interactionSession).Error; err != nil { + return err + } + + reauthenticationToken := webauthn.ReauthenticationToken{ + Base: model.Base{ + ID: "71839ace-d978-4e6f-8fb1-b8648a21031b", + }, + Token: "cross-database-reauthentication-token", + ExpiresAt: farFuture, + UserID: users[0].ID, + } + if err := tx.Create(&reauthenticationToken).Error; err != nil { + return err + } + accessToken := model.OneTimeAccessToken{ Token: "one-time-token", ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)), diff --git a/backend/resources/migrations/postgres/20260722120000_oauth_storage_export_normalization.down.sql b/backend/resources/migrations/postgres/20260722120000_oauth_storage_export_normalization.down.sql new file mode 100644 index 00000000..46a3724d --- /dev/null +++ b/backend/resources/migrations/postgres/20260722120000_oauth_storage_export_normalization.down.sql @@ -0,0 +1 @@ +-- No-op on PostgreSQL diff --git a/backend/resources/migrations/postgres/20260722120000_oauth_storage_export_normalization.up.sql b/backend/resources/migrations/postgres/20260722120000_oauth_storage_export_normalization.up.sql new file mode 100644 index 00000000..85df0180 --- /dev/null +++ b/backend/resources/migrations/postgres/20260722120000_oauth_storage_export_normalization.up.sql @@ -0,0 +1 @@ +-- No-op on PostgreSQL because its OAuth storage types already match the export format diff --git a/backend/resources/migrations/sqlite/20260722120000_oauth_storage_export_normalization.down.sql b/backend/resources/migrations/sqlite/20260722120000_oauth_storage_export_normalization.down.sql new file mode 100644 index 00000000..632ab194 --- /dev/null +++ b/backend/resources/migrations/sqlite/20260722120000_oauth_storage_export_normalization.down.sql @@ -0,0 +1,151 @@ +PRAGMA foreign_keys = OFF; +BEGIN; + +CREATE TABLE reauthentication_tokens_old ( + id TEXT PRIMARY KEY, + created_at DATETIME NOT NULL, + token TEXT NOT NULL UNIQUE, + expires_at INTEGER NOT NULL, + user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE +); + +INSERT INTO reauthentication_tokens_old ( + id, + created_at, + token, + expires_at, + user_id +) +SELECT + id, + created_at, + token, + expires_at, + user_id +FROM reauthentication_tokens; + +DROP TABLE reauthentication_tokens; +ALTER TABLE reauthentication_tokens_old RENAME TO reauthentication_tokens; + +CREATE INDEX idx_reauthentication_tokens_token ON reauthentication_tokens (token); +CREATE INDEX idx_reauthentication_tokens_expires_at ON reauthentication_tokens (expires_at); + +CREATE TABLE oauth2_sessions_old ( + id TEXT NOT NULL PRIMARY KEY, + created_at INTEGER NOT NULL, + kind TEXT NOT NULL, + key TEXT NOT NULL, + request_id TEXT NOT NULL, + access_token_signature TEXT NOT NULL DEFAULT '', + active BOOLEAN NOT NULL DEFAULT TRUE, + request_data TEXT NOT NULL, + expires_at INTEGER +); + +INSERT INTO oauth2_sessions_old ( + id, + created_at, + kind, + key, + request_id, + access_token_signature, + active, + request_data, + expires_at +) +SELECT + id, + created_at, + kind, + key, + request_id, + access_token_signature, + active, + CAST(request_data AS TEXT), + expires_at +FROM oauth2_sessions; + +DROP TABLE oauth2_sessions; +ALTER TABLE oauth2_sessions_old RENAME TO oauth2_sessions; + +CREATE UNIQUE INDEX idx_oauth2_sessions_kind_key ON oauth2_sessions (kind, key); +CREATE INDEX idx_oauth2_sessions_kind_request ON oauth2_sessions (kind, request_id); +CREATE INDEX idx_oauth2_sessions_expires_at ON oauth2_sessions (expires_at); + +CREATE TABLE oauth2_jtis_old ( + id TEXT NOT NULL PRIMARY KEY, + created_at INTEGER NOT NULL, + jti TEXT NOT NULL UNIQUE, + expires_at INTEGER NOT NULL +); + +INSERT INTO oauth2_jtis_old ( + id, + created_at, + jti, + expires_at +) +SELECT + id, + created_at, + jti, + expires_at +FROM oauth2_jtis; + +DROP TABLE oauth2_jtis; +ALTER TABLE oauth2_jtis_old RENAME TO oauth2_jtis; + +CREATE INDEX idx_oauth2_jtis_expires_at ON oauth2_jtis (expires_at); + +CREATE TABLE interaction_sessions_old ( + id TEXT NOT NULL PRIMARY KEY, + created_at INTEGER NOT NULL, + consent_required BOOLEAN NOT NULL DEFAULT FALSE, + reauthentication_required BOOLEAN NOT NULL DEFAULT FALSE, + authentication_required BOOLEAN NOT NULL DEFAULT FALSE, + account_selection_required BOOLEAN NOT NULL DEFAULT FALSE, + scopes TEXT NOT NULL DEFAULT '[]', + client_id TEXT NOT NULL REFERENCES oidc_clients(id) ON DELETE CASCADE, + user_id TEXT REFERENCES users(id) ON DELETE CASCADE, + requested_at INTEGER NOT NULL, + reauthenticated_at INTEGER, + parameters TEXT NOT NULL DEFAULT '{}' +); + +INSERT INTO interaction_sessions_old ( + id, + created_at, + consent_required, + reauthentication_required, + authentication_required, + account_selection_required, + scopes, + client_id, + user_id, + requested_at, + reauthenticated_at, + parameters +) +SELECT + id, + created_at, + consent_required, + reauthentication_required, + authentication_required, + account_selection_required, + CAST(scopes AS TEXT), + client_id, + user_id, + requested_at, + reauthenticated_at, + CAST(parameters AS TEXT) +FROM interaction_sessions; + +DROP TABLE interaction_sessions; +ALTER TABLE interaction_sessions_old RENAME TO interaction_sessions; + +CREATE INDEX idx_interaction_sessions_client_id ON interaction_sessions (client_id); +CREATE INDEX idx_interaction_sessions_user_id ON interaction_sessions (user_id); + +COMMIT; +PRAGMA foreign_keys = ON; diff --git a/backend/resources/migrations/sqlite/20260722120000_oauth_storage_export_normalization.up.sql b/backend/resources/migrations/sqlite/20260722120000_oauth_storage_export_normalization.up.sql new file mode 100644 index 00000000..4fba5aea --- /dev/null +++ b/backend/resources/migrations/sqlite/20260722120000_oauth_storage_export_normalization.up.sql @@ -0,0 +1,152 @@ +PRAGMA foreign_keys = OFF; +BEGIN; + +-- Align JSON and timestamp column types with the export format used for PostgreSQL +CREATE TABLE reauthentication_tokens_new ( + id TEXT PRIMARY KEY, + created_at DATETIME NOT NULL, + token TEXT NOT NULL UNIQUE, + expires_at DATETIME NOT NULL, + user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE +); + +INSERT INTO reauthentication_tokens_new ( + id, + created_at, + token, + expires_at, + user_id +) +SELECT + id, + created_at, + token, + expires_at, + user_id +FROM reauthentication_tokens; + +DROP TABLE reauthentication_tokens; +ALTER TABLE reauthentication_tokens_new RENAME TO reauthentication_tokens; + +CREATE INDEX idx_reauthentication_tokens_token ON reauthentication_tokens (token); +CREATE INDEX idx_reauthentication_tokens_expires_at ON reauthentication_tokens (expires_at); + +CREATE TABLE oauth2_sessions_new ( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME NOT NULL, + kind TEXT NOT NULL, + key TEXT NOT NULL, + request_id TEXT NOT NULL, + access_token_signature TEXT NOT NULL DEFAULT '', + active BOOLEAN NOT NULL DEFAULT TRUE, + request_data BLOB NOT NULL, + expires_at DATETIME +); + +INSERT INTO oauth2_sessions_new ( + id, + created_at, + kind, + key, + request_id, + access_token_signature, + active, + request_data, + expires_at +) +SELECT + id, + created_at, + kind, + key, + request_id, + access_token_signature, + active, + CAST(request_data AS BLOB), + expires_at +FROM oauth2_sessions; + +DROP TABLE oauth2_sessions; +ALTER TABLE oauth2_sessions_new RENAME TO oauth2_sessions; + +CREATE UNIQUE INDEX idx_oauth2_sessions_kind_key ON oauth2_sessions (kind, key); +CREATE INDEX idx_oauth2_sessions_kind_request ON oauth2_sessions (kind, request_id); +CREATE INDEX idx_oauth2_sessions_expires_at ON oauth2_sessions (expires_at); + +CREATE TABLE oauth2_jtis_new ( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME NOT NULL, + jti TEXT NOT NULL UNIQUE, + expires_at DATETIME NOT NULL +); + +INSERT INTO oauth2_jtis_new ( + id, + created_at, + jti, + expires_at +) +SELECT + id, + created_at, + jti, + expires_at +FROM oauth2_jtis; + +DROP TABLE oauth2_jtis; +ALTER TABLE oauth2_jtis_new RENAME TO oauth2_jtis; + +CREATE INDEX idx_oauth2_jtis_expires_at ON oauth2_jtis (expires_at); + +CREATE TABLE interaction_sessions_new ( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME NOT NULL, + consent_required BOOLEAN NOT NULL DEFAULT FALSE, + reauthentication_required BOOLEAN NOT NULL DEFAULT FALSE, + authentication_required BOOLEAN NOT NULL DEFAULT FALSE, + account_selection_required BOOLEAN NOT NULL DEFAULT FALSE, + scopes BLOB NOT NULL DEFAULT X'5B5D', + client_id TEXT NOT NULL REFERENCES oidc_clients(id) ON DELETE CASCADE, + user_id TEXT REFERENCES users(id) ON DELETE CASCADE, + requested_at DATETIME NOT NULL, + reauthenticated_at DATETIME, + parameters BLOB NOT NULL DEFAULT X'7B7D' +); + +INSERT INTO interaction_sessions_new ( + id, + created_at, + consent_required, + reauthentication_required, + authentication_required, + account_selection_required, + scopes, + client_id, + user_id, + requested_at, + reauthenticated_at, + parameters +) +SELECT + id, + created_at, + consent_required, + reauthentication_required, + authentication_required, + account_selection_required, + CAST(scopes AS BLOB), + client_id, + user_id, + requested_at, + reauthenticated_at, + CAST(parameters AS BLOB) +FROM interaction_sessions; + +DROP TABLE interaction_sessions; +ALTER TABLE interaction_sessions_new RENAME TO interaction_sessions; + +CREATE INDEX idx_interaction_sessions_client_id ON interaction_sessions (client_id); +CREATE INDEX idx_interaction_sessions_user_id ON interaction_sessions (user_id); + +COMMIT; +PRAGMA foreign_keys = ON; diff --git a/tests/resources/export/database.json b/tests/resources/export/database.json index 37ebf554..e02a1bdb 100644 --- a/tests/resources/export/database.json +++ b/tests/resources/export/database.json @@ -1,7 +1,15 @@ { "provider": "sqlite", - "version": 20260708130000, - "tableOrder": ["users", "user_groups", "oidc_clients", "signup_tokens", "apis", "api_permissions", "oidc_clients_allowed_api_permissions"], + "version": 20260722120000, + "tableOrder": [ + "users", + "user_groups", + "oidc_clients", + "signup_tokens", + "apis", + "api_permissions", + "oidc_clients_allowed_api_permissions" + ], "tables": { "apis": [ { @@ -264,6 +272,52 @@ "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" } ], + "oauth2_jtis": [ + { + "id": "bd0c8bf2-66ec-487a-9dd5-7d9d78d73543", + "created_at": "2026-07-22T12:00:00Z", + "jti": "cross-database-test-jti", + "expires_at": "2099-01-01T00:00:00Z" + } + ], + "oauth2_sessions": [ + { + "id": "551ab785-c830-47d3-8a07-60c9f3bb4859", + "created_at": "2026-07-22T12:00:00Z", + "kind": "access_token", + "key": "cross-database-test-session", + "request_id": "cross-database-test-request", + "access_token_signature": "", + "active": true, + "request_data": "eyJyZXF1ZXN0IjoidmFsdWUifQ==", + "expires_at": "2099-01-01T00:00:00Z" + } + ], + "interaction_sessions": [ + { + "id": "aaf5dd23-cd1f-4748-a2aa-baa6af94d800", + "created_at": "2026-07-22T12:00:00Z", + "consent_required": true, + "reauthentication_required": false, + "authentication_required": false, + "account_selection_required": false, + "scopes": "WyJvcGVuaWQiXQ==", + "client_id": "3654a746-35d4-4321-ac61-0bdcff2b4055", + "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e", + "requested_at": "2099-01-01T00:00:00Z", + "reauthenticated_at": null, + "parameters": "eyJjbGllbnRfaWQiOiIzNjU0YTc0Ni0zNWQ0LTQzMjEtYWM2MS0wYmRjZmYyYjQwNTUifQ==" + } + ], + "reauthentication_tokens": [ + { + "id": "71839ace-d978-4e6f-8fb1-b8648a21031b", + "created_at": "2026-07-22T12:00:00Z", + "token": "cross-database-reauthentication-token", + "expires_at": "2099-01-01T00:00:00Z", + "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" + } + ], "signup_tokens": [ { "created_at": "2025-11-25T12:39:02Z", diff --git a/tests/specs/cli.spec.ts b/tests/specs/cli.spec.ts index 0e6e7234..5a730af9 100644 --- a/tests/specs/cli.spec.ts +++ b/tests/specs/cli.spec.ts @@ -59,7 +59,7 @@ test('Export via stdout', async ({ baseURL }) => { compareExports(exampleExportPath, stdoutExtractPath); }); -test('Import', async () => { +test('Import SQLite export', async () => { // Reset the backend without seeding await cleanupBackend({ skipSeed: true }); @@ -83,7 +83,7 @@ test('Import', async () => { compareExports(exampleExportPath, exportExtracted); }); -test('Import via stdin', async () => { +test('Import SQLite export via stdin', async () => { await cleanupBackend({ skipSeed: true }); const exampleExportArchivePath = path.join(tmpDir, 'example-export-stdin.zip');