tests: use describe.each for database test iteration
Refactors all test files that use TestDatabases/TestCaches with it.each(databases.eachSupportedId()) to instead use describe.each at the outer level. This ensures that all tests for one database engine complete before moving to the next, rather than interleaving engines across individual tests. This reduces the number of concurrent database connections and should help with test timeout issues in CI. The TestDatabases.create() call is hoisted to module scope so the describe.each can iterate over supported IDs at the top level. 40 files changed across packages/backend-defaults, packages/backend-test-utils, and multiple plugins. Signed-off-by: Fredrik Adelöw <freben@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -27,12 +27,12 @@ const keyBase = {
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
describe('DatabaseKeyStore', () => {
|
||||
const databases = TestDatabases.create();
|
||||
const databases = TestDatabases.create();
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'should store a key, %p',
|
||||
async databaseId => {
|
||||
describe.each(databases.eachSupportedId())(
|
||||
'DatabaseKeyStore, %p',
|
||||
databaseId => {
|
||||
it('should store a key', async () => {
|
||||
const knex = await databases.init(databaseId);
|
||||
await AuthDatabase.runMigrations(knex);
|
||||
|
||||
@@ -53,12 +53,9 @@ describe('DatabaseKeyStore', () => {
|
||||
DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds,
|
||||
),
|
||||
).toBeLessThan(10);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'should remove stored keys, %p',
|
||||
async databaseId => {
|
||||
it('should remove stored keys', async () => {
|
||||
const knex = await databases.init(databaseId);
|
||||
await AuthDatabase.runMigrations(knex);
|
||||
|
||||
@@ -124,6 +121,6 @@ describe('DatabaseKeyStore', () => {
|
||||
await expect(store.listKeys()).resolves.toEqual({
|
||||
items: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -24,89 +24,80 @@ import { mockServices, TestDatabases } from '@backstage/backend-test-utils';
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
describe('KeyStores', () => {
|
||||
const databases = TestDatabases.create();
|
||||
const databases = TestDatabases.create();
|
||||
|
||||
const defaultConfigOptions = {
|
||||
auth: {
|
||||
keyStore: {
|
||||
provider: 'memory',
|
||||
},
|
||||
const defaultConfigOptions = {
|
||||
auth: {
|
||||
keyStore: {
|
||||
provider: 'memory',
|
||||
},
|
||||
};
|
||||
const defaultConfig = new ConfigReader(defaultConfigOptions);
|
||||
},
|
||||
};
|
||||
const defaultConfig = new ConfigReader(defaultConfigOptions);
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'reads auth section from config, %p',
|
||||
async databaseId => {
|
||||
const knex = await databases.init(databaseId);
|
||||
const configSpy = jest.spyOn(defaultConfig, 'getOptionalConfig');
|
||||
const keyStore = await KeyStores.fromConfig(defaultConfig, {
|
||||
logger: mockServices.logger.mock(),
|
||||
database: AuthDatabase.create(mockServices.database({ knex })),
|
||||
});
|
||||
describe.each(databases.eachSupportedId())('KeyStores, %p', databaseId => {
|
||||
it('reads auth section from config', async () => {
|
||||
const knex = await databases.init(databaseId);
|
||||
const configSpy = jest.spyOn(defaultConfig, 'getOptionalConfig');
|
||||
const keyStore = await KeyStores.fromConfig(defaultConfig, {
|
||||
logger: mockServices.logger.mock(),
|
||||
database: AuthDatabase.create(mockServices.database({ knex })),
|
||||
});
|
||||
|
||||
expect(keyStore).toBeInstanceOf(MemoryKeyStore);
|
||||
expect(configSpy).toHaveBeenCalledWith('auth.keyStore');
|
||||
expect(
|
||||
defaultConfig
|
||||
.getOptionalConfig('auth.keyStore')
|
||||
?.getOptionalString('provider'),
|
||||
).toBe(defaultConfigOptions.auth.keyStore.provider);
|
||||
},
|
||||
);
|
||||
expect(keyStore).toBeInstanceOf(MemoryKeyStore);
|
||||
expect(configSpy).toHaveBeenCalledWith('auth.keyStore');
|
||||
expect(
|
||||
defaultConfig
|
||||
.getOptionalConfig('auth.keyStore')
|
||||
?.getOptionalString('provider'),
|
||||
).toBe(defaultConfigOptions.auth.keyStore.provider);
|
||||
});
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'can handle without auth config, %p',
|
||||
async databaseId => {
|
||||
const knex = await databases.init(databaseId);
|
||||
const keyStore = await KeyStores.fromConfig(new ConfigReader({}), {
|
||||
logger: mockServices.logger.mock(),
|
||||
database: AuthDatabase.create(mockServices.database({ knex })),
|
||||
});
|
||||
expect(keyStore).toBeInstanceOf(DatabaseKeyStore);
|
||||
},
|
||||
);
|
||||
it('can handle without auth config', async () => {
|
||||
const knex = await databases.init(databaseId);
|
||||
const keyStore = await KeyStores.fromConfig(new ConfigReader({}), {
|
||||
logger: mockServices.logger.mock(),
|
||||
database: AuthDatabase.create(mockServices.database({ knex })),
|
||||
});
|
||||
expect(keyStore).toBeInstanceOf(DatabaseKeyStore);
|
||||
});
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'can handle additional provider config, %p',
|
||||
async databaseId => {
|
||||
const knex = await databases.init(databaseId);
|
||||
jest.spyOn(FirestoreKeyStore, 'verifyConnection').mockResolvedValue();
|
||||
const createSpy = jest.spyOn(FirestoreKeyStore, 'create');
|
||||
it('can handle additional provider config', async () => {
|
||||
const knex = await databases.init(databaseId);
|
||||
jest.spyOn(FirestoreKeyStore, 'verifyConnection').mockResolvedValue();
|
||||
const createSpy = jest.spyOn(FirestoreKeyStore, 'create');
|
||||
|
||||
const configOptions = {
|
||||
auth: {
|
||||
keyStore: {
|
||||
provider: 'firestore',
|
||||
firestore: {
|
||||
projectId: 'my-project',
|
||||
keyFilename: 'cred.json',
|
||||
path: 'my-path',
|
||||
timeout: 100,
|
||||
host: 'localhost',
|
||||
port: 8088,
|
||||
ssl: false,
|
||||
},
|
||||
const configOptions = {
|
||||
auth: {
|
||||
keyStore: {
|
||||
provider: 'firestore',
|
||||
firestore: {
|
||||
projectId: 'my-project',
|
||||
keyFilename: 'cred.json',
|
||||
path: 'my-path',
|
||||
timeout: 100,
|
||||
host: 'localhost',
|
||||
port: 8088,
|
||||
ssl: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const config = new ConfigReader(configOptions);
|
||||
const keyStore = await KeyStores.fromConfig(config, {
|
||||
logger: mockServices.logger.mock(),
|
||||
database: AuthDatabase.create(mockServices.database({ knex })),
|
||||
});
|
||||
},
|
||||
};
|
||||
const config = new ConfigReader(configOptions);
|
||||
const keyStore = await KeyStores.fromConfig(config, {
|
||||
logger: mockServices.logger.mock(),
|
||||
database: AuthDatabase.create(mockServices.database({ knex })),
|
||||
});
|
||||
|
||||
expect(keyStore).toBeInstanceOf(FirestoreKeyStore);
|
||||
expect(createSpy).toHaveBeenCalledWith(
|
||||
configOptions.auth.keyStore.firestore,
|
||||
);
|
||||
expect(
|
||||
config
|
||||
.getOptionalConfig('auth.keyStore')
|
||||
?.getOptionalConfig('firestore')
|
||||
?.getOptionalString('projectId'),
|
||||
).toBe(configOptions.auth.keyStore.firestore.projectId);
|
||||
},
|
||||
);
|
||||
expect(keyStore).toBeInstanceOf(FirestoreKeyStore);
|
||||
expect(createSpy).toHaveBeenCalledWith(
|
||||
configOptions.auth.keyStore.firestore,
|
||||
);
|
||||
expect(
|
||||
config
|
||||
.getOptionalConfig('auth.keyStore')
|
||||
?.getOptionalConfig('firestore')
|
||||
?.getOptionalString('projectId'),
|
||||
).toBe(configOptions.auth.keyStore.firestore.projectId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,179 +41,154 @@ async function migrateUntilBefore(knex: Knex, target: string): Promise<void> {
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
describe('migrations', () => {
|
||||
const databases = TestDatabases.create();
|
||||
const databases = TestDatabases.create();
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'20230428155633_sessions.js, %p',
|
||||
async databaseId => {
|
||||
const knex = await databases.init(databaseId);
|
||||
describe.each(databases.eachSupportedId())('migrations, %p', databaseId => {
|
||||
it('20230428155633_sessions.js', async () => {
|
||||
const knex = await databases.init(databaseId);
|
||||
|
||||
await migrateUntilBefore(knex, '20230428155633_sessions.js');
|
||||
await migrateUntilBefore(knex, '20230428155633_sessions.js');
|
||||
await migrateUpOnce(knex);
|
||||
|
||||
// Ensure that large cookies are supported
|
||||
const data = `{"cookie":"${'a'.repeat(100_000)}"}`;
|
||||
await knex
|
||||
.insert({ sid: 'abc', expired: knex.fn.now(), sess: data })
|
||||
.into('sessions');
|
||||
await knex
|
||||
.insert({ sid: 'def', expired: knex.fn.now(), sess: data })
|
||||
.into('sessions');
|
||||
|
||||
await expect(knex('sessions').orderBy('sid', 'asc')).resolves.toEqual([
|
||||
{ sid: 'abc', expired: expect.anything(), sess: data },
|
||||
{ sid: 'def', expired: expect.anything(), sess: data },
|
||||
]);
|
||||
|
||||
await migrateDownOnce(knex);
|
||||
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
it('20240510120825_user_info.js', async () => {
|
||||
const knex = await databases.init(databaseId);
|
||||
|
||||
await migrateUntilBefore(knex, '20240510120825_user_info.js');
|
||||
await migrateUpOnce(knex);
|
||||
|
||||
const user_info = JSON.stringify({
|
||||
claims: {
|
||||
ent: ['group:default/group1', 'group:default/group2'],
|
||||
},
|
||||
});
|
||||
|
||||
await knex
|
||||
.insert({
|
||||
user_entity_ref: 'user:default/backstage-user',
|
||||
user_info,
|
||||
exp: knex.fn.now(),
|
||||
})
|
||||
.into('user_info');
|
||||
|
||||
await expect(knex('user_info')).resolves.toEqual([
|
||||
{
|
||||
user_entity_ref: 'user:default/backstage-user',
|
||||
user_info,
|
||||
exp: expect.anything(),
|
||||
},
|
||||
]);
|
||||
|
||||
await migrateDownOnce(knex);
|
||||
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
it('20250707164600_user_created_at.js', async () => {
|
||||
const knex = await databases.init(databaseId);
|
||||
await migrateUntilBefore(knex, '20250707164600_user_created_at.js');
|
||||
|
||||
if (knex.client.config.client.includes('sqlite')) {
|
||||
// Sqlite doesn't support adding a column with non-constant default when table has data
|
||||
// so we just test that the migration runs without errors
|
||||
await migrateUpOnce(knex);
|
||||
|
||||
// Ensure that large cookies are supported
|
||||
const data = `{"cookie":"${'a'.repeat(100_000)}"}`;
|
||||
await knex
|
||||
.insert({ sid: 'abc', expired: knex.fn.now(), sess: data })
|
||||
.into('sessions');
|
||||
await knex
|
||||
.insert({ sid: 'def', expired: knex.fn.now(), sess: data })
|
||||
.into('sessions');
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(knex('sessions').orderBy('sid', 'asc')).resolves.toEqual([
|
||||
{ sid: 'abc', expired: expect.anything(), sess: data },
|
||||
{ sid: 'def', expired: expect.anything(), sess: data },
|
||||
]);
|
||||
const user_info = JSON.stringify({
|
||||
claims: {
|
||||
ent: ['group:default/group1', 'group:default/group2'],
|
||||
},
|
||||
});
|
||||
|
||||
await migrateDownOnce(knex);
|
||||
await knex
|
||||
.insert({
|
||||
user_entity_ref: 'user:default/backstage-user',
|
||||
user_info,
|
||||
exp: knex.fn.now(),
|
||||
})
|
||||
.into('user_info');
|
||||
|
||||
await knex.destroy();
|
||||
},
|
||||
);
|
||||
const { exp } = await knex('user_info').first();
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'20240510120825_user_info.js, %p',
|
||||
async databaseId => {
|
||||
const knex = await databases.init(databaseId);
|
||||
await migrateUpOnce(knex);
|
||||
|
||||
await migrateUntilBefore(knex, '20240510120825_user_info.js');
|
||||
await migrateUpOnce(knex);
|
||||
const { created_at, updated_at } = await knex('user_info').first();
|
||||
|
||||
const user_info = JSON.stringify({
|
||||
claims: {
|
||||
ent: ['group:default/group1', 'group:default/group2'],
|
||||
},
|
||||
});
|
||||
expect(updated_at).toEqual(exp);
|
||||
expect(created_at).toBeDefined();
|
||||
|
||||
await knex
|
||||
.insert({
|
||||
user_entity_ref: 'user:default/backstage-user',
|
||||
user_info,
|
||||
exp: knex.fn.now(),
|
||||
})
|
||||
.into('user_info');
|
||||
await knex
|
||||
.insert({
|
||||
user_entity_ref: 'user:default/backstage-user',
|
||||
user_info,
|
||||
updated_at: knex.fn.now(),
|
||||
})
|
||||
.into('user_info')
|
||||
.onConflict(['user_entity_ref'])
|
||||
.merge();
|
||||
|
||||
await expect(knex('user_info')).resolves.toEqual([
|
||||
{
|
||||
user_entity_ref: 'user:default/backstage-user',
|
||||
user_info,
|
||||
exp: expect.anything(),
|
||||
},
|
||||
]);
|
||||
await knex
|
||||
.insert({
|
||||
user_entity_ref: 'user:default/backstage-user-2',
|
||||
user_info,
|
||||
updated_at: knex.fn.now(),
|
||||
})
|
||||
.into('user_info');
|
||||
|
||||
await migrateDownOnce(knex);
|
||||
await expect(
|
||||
knex('user_info').select('created_at', 'updated_at'),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
created_at: expect.any(Date),
|
||||
updated_at: expect.any(Date),
|
||||
},
|
||||
{
|
||||
created_at: expect.any(Date),
|
||||
updated_at: expect.any(Date),
|
||||
},
|
||||
]);
|
||||
|
||||
await knex.destroy();
|
||||
},
|
||||
);
|
||||
await migrateDownOnce(knex);
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'20250707164600_user_created_at.js, %p',
|
||||
async databaseId => {
|
||||
const knex = await databases.init(databaseId);
|
||||
await migrateUntilBefore(knex, '20250707164600_user_created_at.js');
|
||||
await expect(knex('user_info').select('exp')).resolves.toEqual([
|
||||
{ exp: expect.any(Date) },
|
||||
{ exp: expect.any(Date) },
|
||||
]);
|
||||
|
||||
if (knex.client.config.client.includes('sqlite')) {
|
||||
// Sqlite doesn't support adding a column with non-constant default when table has data
|
||||
// so we just test that the migration runs without errors
|
||||
await migrateUpOnce(knex);
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
it('20250909120000_oidc_client_registration.js', async () => {
|
||||
const knex = await databases.init(databaseId);
|
||||
|
||||
const user_info = JSON.stringify({
|
||||
claims: {
|
||||
ent: ['group:default/group1', 'group:default/group2'],
|
||||
},
|
||||
});
|
||||
await migrateUntilBefore(
|
||||
knex,
|
||||
'20250909120000_oidc_client_registration.js',
|
||||
);
|
||||
await migrateUpOnce(knex);
|
||||
|
||||
await knex
|
||||
.insert({
|
||||
user_entity_ref: 'user:default/backstage-user',
|
||||
user_info,
|
||||
exp: knex.fn.now(),
|
||||
})
|
||||
.into('user_info');
|
||||
|
||||
const { exp } = await knex('user_info').first();
|
||||
|
||||
await migrateUpOnce(knex);
|
||||
|
||||
const { created_at, updated_at } = await knex('user_info').first();
|
||||
|
||||
expect(updated_at).toEqual(exp);
|
||||
expect(created_at).toBeDefined();
|
||||
|
||||
await knex
|
||||
.insert({
|
||||
user_entity_ref: 'user:default/backstage-user',
|
||||
user_info,
|
||||
updated_at: knex.fn.now(),
|
||||
})
|
||||
.into('user_info')
|
||||
.onConflict(['user_entity_ref'])
|
||||
.merge();
|
||||
|
||||
await knex
|
||||
.insert({
|
||||
user_entity_ref: 'user:default/backstage-user-2',
|
||||
user_info,
|
||||
updated_at: knex.fn.now(),
|
||||
})
|
||||
.into('user_info');
|
||||
|
||||
await expect(
|
||||
knex('user_info').select('created_at', 'updated_at'),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
created_at: expect.any(Date),
|
||||
updated_at: expect.any(Date),
|
||||
},
|
||||
{
|
||||
created_at: expect.any(Date),
|
||||
updated_at: expect.any(Date),
|
||||
},
|
||||
]);
|
||||
|
||||
await migrateDownOnce(knex);
|
||||
|
||||
await expect(knex('user_info').select('exp')).resolves.toEqual([
|
||||
{ exp: expect.any(Date) },
|
||||
{ exp: expect.any(Date) },
|
||||
]);
|
||||
|
||||
await knex.destroy();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'20250909120000_oidc_client_registration.js, %p',
|
||||
async databaseId => {
|
||||
const knex = await databases.init(databaseId);
|
||||
|
||||
await migrateUntilBefore(
|
||||
knex,
|
||||
'20250909120000_oidc_client_registration.js',
|
||||
);
|
||||
await migrateUpOnce(knex);
|
||||
|
||||
await knex
|
||||
.insert({
|
||||
client_id: 'test-client-id',
|
||||
client_secret: 'test-client-secret',
|
||||
client_name: 'Test Client',
|
||||
response_types: JSON.stringify(['code']),
|
||||
grant_types: JSON.stringify(['authorization_code']),
|
||||
redirect_uris: JSON.stringify(['https://example.com/callback']),
|
||||
scope: 'openid profile',
|
||||
metadata: JSON.stringify({ description: 'Test client' }),
|
||||
})
|
||||
.into('oidc_clients');
|
||||
|
||||
await expect(
|
||||
knex('oidc_clients').where('client_id', 'test-client-id').first(),
|
||||
).resolves.toEqual({
|
||||
await knex
|
||||
.insert({
|
||||
client_id: 'test-client-id',
|
||||
client_secret: 'test-client-secret',
|
||||
client_name: 'Test Client',
|
||||
@@ -222,230 +197,235 @@ describe('migrations', () => {
|
||||
redirect_uris: JSON.stringify(['https://example.com/callback']),
|
||||
scope: 'openid profile',
|
||||
metadata: JSON.stringify({ description: 'Test client' }),
|
||||
});
|
||||
})
|
||||
.into('oidc_clients');
|
||||
|
||||
await knex
|
||||
.insert({
|
||||
id: 'test-session-id',
|
||||
client_id: 'test-client-id',
|
||||
user_entity_ref: 'user:default/test-user',
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
scope: 'openid',
|
||||
state: 'test-state',
|
||||
response_type: 'code',
|
||||
code_challenge: 'test-challenge',
|
||||
code_challenge_method: 'S256',
|
||||
nonce: 'test-nonce',
|
||||
status: 'pending',
|
||||
expires_at: new Date(Date.now() + 3600000),
|
||||
})
|
||||
.into('oauth_authorization_sessions');
|
||||
await expect(
|
||||
knex('oidc_clients').where('client_id', 'test-client-id').first(),
|
||||
).resolves.toEqual({
|
||||
client_id: 'test-client-id',
|
||||
client_secret: 'test-client-secret',
|
||||
client_name: 'Test Client',
|
||||
response_types: JSON.stringify(['code']),
|
||||
grant_types: JSON.stringify(['authorization_code']),
|
||||
redirect_uris: JSON.stringify(['https://example.com/callback']),
|
||||
scope: 'openid profile',
|
||||
metadata: JSON.stringify({ description: 'Test client' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
knex('oauth_authorization_sessions')
|
||||
.where('id', 'test-session-id')
|
||||
.first(),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'test-session-id',
|
||||
client_id: 'test-client-id',
|
||||
user_entity_ref: 'user:default/test-user',
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
scope: 'openid',
|
||||
state: 'test-state',
|
||||
response_type: 'code',
|
||||
code_challenge: 'test-challenge',
|
||||
code_challenge_method: 'S256',
|
||||
nonce: 'test-nonce',
|
||||
status: 'pending',
|
||||
}),
|
||||
);
|
||||
await knex
|
||||
.insert({
|
||||
id: 'test-session-id',
|
||||
client_id: 'test-client-id',
|
||||
user_entity_ref: 'user:default/test-user',
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
scope: 'openid',
|
||||
state: 'test-state',
|
||||
response_type: 'code',
|
||||
code_challenge: 'test-challenge',
|
||||
code_challenge_method: 'S256',
|
||||
nonce: 'test-nonce',
|
||||
status: 'pending',
|
||||
expires_at: new Date(Date.now() + 3600000),
|
||||
})
|
||||
.into('oauth_authorization_sessions');
|
||||
|
||||
await knex
|
||||
.insert({
|
||||
code: 'test-auth-code',
|
||||
session_id: 'test-session-id',
|
||||
expires_at: new Date(Date.now() + 600000),
|
||||
used: false,
|
||||
})
|
||||
.into('oidc_authorization_codes');
|
||||
|
||||
await expect(
|
||||
knex('oidc_authorization_codes')
|
||||
.where('code', 'test-auth-code')
|
||||
.first(),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
code: 'test-auth-code',
|
||||
session_id: 'test-session-id',
|
||||
}),
|
||||
);
|
||||
|
||||
await knex('oauth_authorization_sessions')
|
||||
await expect(
|
||||
knex('oauth_authorization_sessions')
|
||||
.where('id', 'test-session-id')
|
||||
.del();
|
||||
.first(),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'test-session-id',
|
||||
client_id: 'test-client-id',
|
||||
user_entity_ref: 'user:default/test-user',
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
scope: 'openid',
|
||||
state: 'test-state',
|
||||
response_type: 'code',
|
||||
code_challenge: 'test-challenge',
|
||||
code_challenge_method: 'S256',
|
||||
nonce: 'test-nonce',
|
||||
status: 'pending',
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
knex('oidc_authorization_codes').where('session_id', 'test-session-id'),
|
||||
).resolves.toHaveLength(0);
|
||||
await knex
|
||||
.insert({
|
||||
code: 'test-auth-code',
|
||||
session_id: 'test-session-id',
|
||||
expires_at: new Date(Date.now() + 600000),
|
||||
used: false,
|
||||
})
|
||||
.into('oidc_authorization_codes');
|
||||
|
||||
await migrateDownOnce(knex);
|
||||
await expect(
|
||||
knex('oidc_authorization_codes').where('code', 'test-auth-code').first(),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
code: 'test-auth-code',
|
||||
session_id: 'test-session-id',
|
||||
}),
|
||||
);
|
||||
|
||||
const tables = [
|
||||
'oidc_clients',
|
||||
'oauth_authorization_sessions',
|
||||
'oidc_authorization_codes',
|
||||
];
|
||||
await knex('oauth_authorization_sessions')
|
||||
.where('id', 'test-session-id')
|
||||
.del();
|
||||
|
||||
for (const table of tables) {
|
||||
await expect(knex.schema.hasTable(table)).resolves.toBe(false);
|
||||
}
|
||||
await expect(
|
||||
knex('oidc_authorization_codes').where('session_id', 'test-session-id'),
|
||||
).resolves.toHaveLength(0);
|
||||
|
||||
await knex.destroy();
|
||||
},
|
||||
);
|
||||
await migrateDownOnce(knex);
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'20251118120000_oauth_state_text.js, %p',
|
||||
async databaseId => {
|
||||
const knex = await databases.init(databaseId);
|
||||
const tables = [
|
||||
'oidc_clients',
|
||||
'oauth_authorization_sessions',
|
||||
'oidc_authorization_codes',
|
||||
];
|
||||
|
||||
await migrateUntilBefore(knex, '20251118120000_oauth_state_text.js');
|
||||
for (const table of tables) {
|
||||
await expect(knex.schema.hasTable(table)).resolves.toBe(false);
|
||||
}
|
||||
|
||||
// First create a client for the foreign key constraint
|
||||
await knex
|
||||
.insert({
|
||||
client_id: 'test-client-id',
|
||||
client_secret: 'test-client-secret',
|
||||
client_name: 'Test Client',
|
||||
response_types: JSON.stringify(['code']),
|
||||
grant_types: JSON.stringify(['authorization_code']),
|
||||
redirect_uris: JSON.stringify(['https://example.com/callback']),
|
||||
})
|
||||
.into('oidc_clients');
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
// Insert a session with state before migration
|
||||
const existingState = 'existing-short-state';
|
||||
await knex
|
||||
.insert({
|
||||
id: 'test-existing-session',
|
||||
client_id: 'test-client-id',
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
state: existingState,
|
||||
response_type: 'code',
|
||||
status: 'pending',
|
||||
expires_at: new Date(Date.now() + 3600000),
|
||||
})
|
||||
.into('oauth_authorization_sessions');
|
||||
it('20251118120000_oauth_state_text.js', async () => {
|
||||
const knex = await databases.init(databaseId);
|
||||
|
||||
// Apply the migration that changes state to TEXT
|
||||
await migrateUpOnce(knex);
|
||||
await migrateUntilBefore(knex, '20251118120000_oauth_state_text.js');
|
||||
|
||||
// Verify existing state persists after migration
|
||||
await expect(
|
||||
knex('oauth_authorization_sessions')
|
||||
.where('id', 'test-existing-session')
|
||||
.first(),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'test-existing-session',
|
||||
state: existingState,
|
||||
}),
|
||||
);
|
||||
// First create a client for the foreign key constraint
|
||||
await knex
|
||||
.insert({
|
||||
client_id: 'test-client-id',
|
||||
client_secret: 'test-client-secret',
|
||||
client_name: 'Test Client',
|
||||
response_types: JSON.stringify(['code']),
|
||||
grant_types: JSON.stringify(['authorization_code']),
|
||||
redirect_uris: JSON.stringify(['https://example.com/callback']),
|
||||
})
|
||||
.into('oidc_clients');
|
||||
|
||||
// Test inserting a state parameter longer than 255 characters
|
||||
// This is based on the real-world example from the issue
|
||||
const longState = 'a'.repeat(280);
|
||||
// Insert a session with state before migration
|
||||
const existingState = 'existing-short-state';
|
||||
await knex
|
||||
.insert({
|
||||
id: 'test-existing-session',
|
||||
client_id: 'test-client-id',
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
state: existingState,
|
||||
response_type: 'code',
|
||||
status: 'pending',
|
||||
expires_at: new Date(Date.now() + 3600000),
|
||||
})
|
||||
.into('oauth_authorization_sessions');
|
||||
|
||||
await knex
|
||||
.insert({
|
||||
id: 'test-long-state-session',
|
||||
client_id: 'test-client-id',
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
state: longState,
|
||||
response_type: 'code',
|
||||
status: 'pending',
|
||||
expires_at: new Date(Date.now() + 3600000),
|
||||
})
|
||||
.into('oauth_authorization_sessions');
|
||||
// Apply the migration that changes state to TEXT
|
||||
await migrateUpOnce(knex);
|
||||
|
||||
await expect(
|
||||
knex('oauth_authorization_sessions')
|
||||
.where('id', 'test-long-state-session')
|
||||
.first(),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'test-long-state-session',
|
||||
state: longState,
|
||||
}),
|
||||
);
|
||||
// Verify existing state persists after migration
|
||||
await expect(
|
||||
knex('oauth_authorization_sessions')
|
||||
.where('id', 'test-existing-session')
|
||||
.first(),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'test-existing-session',
|
||||
state: existingState,
|
||||
}),
|
||||
);
|
||||
|
||||
await migrateDownOnce(knex);
|
||||
// Test inserting a state parameter longer than 255 characters
|
||||
// This is based on the real-world example from the issue
|
||||
const longState = 'a'.repeat(280);
|
||||
|
||||
await knex.destroy();
|
||||
},
|
||||
);
|
||||
await knex
|
||||
.insert({
|
||||
id: 'test-long-state-session',
|
||||
client_id: 'test-client-id',
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
state: longState,
|
||||
response_type: 'code',
|
||||
status: 'pending',
|
||||
expires_at: new Date(Date.now() + 3600000),
|
||||
})
|
||||
.into('oauth_authorization_sessions');
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'20251217120000_drop_oidc_clients_fk.js, %p',
|
||||
async databaseId => {
|
||||
const knex = await databases.init(databaseId);
|
||||
await expect(
|
||||
knex('oauth_authorization_sessions')
|
||||
.where('id', 'test-long-state-session')
|
||||
.first(),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'test-long-state-session',
|
||||
state: longState,
|
||||
}),
|
||||
);
|
||||
|
||||
await migrateUntilBefore(knex, '20251217120000_drop_oidc_clients_fk.js');
|
||||
await migrateDownOnce(knex);
|
||||
|
||||
// Create a client for DCR sessions
|
||||
await knex
|
||||
.insert({
|
||||
client_id: 'dcr-client-id',
|
||||
client_secret: 'test-client-secret',
|
||||
client_name: 'DCR Client',
|
||||
response_types: JSON.stringify(['code']),
|
||||
grant_types: JSON.stringify(['authorization_code']),
|
||||
redirect_uris: JSON.stringify(['https://example.com/callback']),
|
||||
})
|
||||
.into('oidc_clients');
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
// Create a DCR session (has matching client in oidc_clients)
|
||||
await knex
|
||||
.insert({
|
||||
id: 'dcr-session',
|
||||
client_id: 'dcr-client-id',
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
response_type: 'code',
|
||||
status: 'pending',
|
||||
expires_at: new Date(Date.now() + 3600000),
|
||||
})
|
||||
.into('oauth_authorization_sessions');
|
||||
it('20251217120000_drop_oidc_clients_fk.js', async () => {
|
||||
const knex = await databases.init(databaseId);
|
||||
|
||||
// Apply migration - drops FK constraint
|
||||
await migrateUpOnce(knex);
|
||||
await migrateUntilBefore(knex, '20251217120000_drop_oidc_clients_fk.js');
|
||||
|
||||
// Now we can insert a CIMD session (URL-based client_id not in oidc_clients)
|
||||
await knex
|
||||
.insert({
|
||||
id: 'cimd-session',
|
||||
client_id: 'https://example.com/.well-known/oauth-client/cli',
|
||||
redirect_uri: 'http://localhost:8080/callback',
|
||||
response_type: 'code',
|
||||
status: 'pending',
|
||||
expires_at: new Date(Date.now() + 3600000),
|
||||
})
|
||||
.into('oauth_authorization_sessions');
|
||||
// Create a client for DCR sessions
|
||||
await knex
|
||||
.insert({
|
||||
client_id: 'dcr-client-id',
|
||||
client_secret: 'test-client-secret',
|
||||
client_name: 'DCR Client',
|
||||
response_types: JSON.stringify(['code']),
|
||||
grant_types: JSON.stringify(['authorization_code']),
|
||||
redirect_uris: JSON.stringify(['https://example.com/callback']),
|
||||
})
|
||||
.into('oidc_clients');
|
||||
|
||||
// Verify both sessions exist
|
||||
await expect(
|
||||
knex('oauth_authorization_sessions').select('id').orderBy('id'),
|
||||
).resolves.toEqual([{ id: 'cimd-session' }, { id: 'dcr-session' }]);
|
||||
// Create a DCR session (has matching client in oidc_clients)
|
||||
await knex
|
||||
.insert({
|
||||
id: 'dcr-session',
|
||||
client_id: 'dcr-client-id',
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
response_type: 'code',
|
||||
status: 'pending',
|
||||
expires_at: new Date(Date.now() + 3600000),
|
||||
})
|
||||
.into('oauth_authorization_sessions');
|
||||
|
||||
// Rollback - should delete CIMD sessions and re-add FK
|
||||
await migrateDownOnce(knex);
|
||||
// Apply migration - drops FK constraint
|
||||
await migrateUpOnce(knex);
|
||||
|
||||
// CIMD session should be deleted, DCR session should remain
|
||||
await expect(
|
||||
knex('oauth_authorization_sessions').select('id'),
|
||||
).resolves.toEqual([{ id: 'dcr-session' }]);
|
||||
// Now we can insert a CIMD session (URL-based client_id not in oidc_clients)
|
||||
await knex
|
||||
.insert({
|
||||
id: 'cimd-session',
|
||||
client_id: 'https://example.com/.well-known/oauth-client/cli',
|
||||
redirect_uri: 'http://localhost:8080/callback',
|
||||
response_type: 'code',
|
||||
status: 'pending',
|
||||
expires_at: new Date(Date.now() + 3600000),
|
||||
})
|
||||
.into('oauth_authorization_sessions');
|
||||
|
||||
await knex.destroy();
|
||||
},
|
||||
);
|
||||
// Verify both sessions exist
|
||||
await expect(
|
||||
knex('oauth_authorization_sessions').select('id').orderBy('id'),
|
||||
).resolves.toEqual([{ id: 'cimd-session' }, { id: 'dcr-session' }]);
|
||||
|
||||
// Rollback - should delete CIMD sessions and re-add FK
|
||||
await migrateDownOnce(knex);
|
||||
|
||||
// CIMD session should be deleted, DCR session should remain
|
||||
await expect(
|
||||
knex('oauth_authorization_sessions').select('id'),
|
||||
).resolves.toEqual([{ id: 'dcr-session' }]);
|
||||
|
||||
await knex.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user