From 59274d23498bb4708dbcc6bd11153ec344126817 Mon Sep 17 00:00:00 2001 From: Arnaud Dezandee <4415204+arnaud-dezandee@users.noreply.github.com> Date: Fri, 3 Apr 2026 08:19:13 +0200 Subject: [PATCH] refactor(terraform-provider): extract URL constants and rename query methods (#42317) * refactor(terraform-provider): extract URL constants and rename query methods * refactor(terraform-provider): consolidate repetitive test cases * refactor(terraform-provider): improve parameterized test readability --- .../terraform-provider/index.spec.ts | 236 ++++++++---------- .../datasource/terraform-provider/index.ts | 60 ++--- 2 files changed, 137 insertions(+), 159 deletions(-) diff --git a/lib/modules/datasource/terraform-provider/index.spec.ts b/lib/modules/datasource/terraform-provider/index.spec.ts index c5a8b69b79..191ab262b3 100644 --- a/lib/modules/datasource/terraform-provider/index.spec.ts +++ b/lib/modules/datasource/terraform-provider/index.spec.ts @@ -18,64 +18,77 @@ const terraformProviderDatasource = new TerraformProviderDatasource(); const primaryUrl = terraformProviderDatasource.defaultRegistryUrls[0]; const secondaryUrl = terraformProviderDatasource.defaultRegistryUrls[1]; +type MockVariant = 'empty' | '404' | 'error'; + +function mockDefaultRegistryLookup(variant: MockVariant): void { + const primaryScope = httpMock + .scope(primaryUrl) + .get('/v1/providers/hashicorp/azurerm'); + const primarySd = httpMock + .scope(primaryUrl) + .get('/.well-known/terraform.json'); + const secondaryScope = httpMock + .scope(secondaryUrl) + .get('/terraform-provider-azurerm/index.json'); + + if (variant === 'empty') { + primaryScope.reply(200, {}); + primarySd.reply(200, serviceDiscoveryResult); + secondaryScope.reply(200, {}); + return; + } + + if (variant === '404') { + primaryScope.reply(404); + primarySd.reply(200, serviceDiscoveryResult); + secondaryScope.reply(404); + return; + } + + primaryScope.replyWithError(''); + primarySd.reply(200, serviceDiscoveryResult); + secondaryScope.replyWithError(''); +} + +function mockThirdPartyRegistryLookup(variant: MockVariant): void { + const scope = httpMock + .scope('https://registry.company.com') + .get('/v1/providers/hashicorp/azurerm/versions'); + + if (variant === 'empty') { + scope.reply(200, {}); + } else if (variant === '404') { + scope.reply(404); + } else { + scope.replyWithError(''); + } + + httpMock + .scope('https://registry.company.com') + .get('/.well-known/terraform.json') + .reply(200, serviceDiscoveryResult); +} + describe('modules/datasource/terraform-provider/index', () => { describe('getReleases', () => { - it('returns null for empty result', async () => { - httpMock - .scope(primaryUrl) - .get('/v1/providers/hashicorp/azurerm') - .reply(200, {}) - .get('/.well-known/terraform.json') - .reply(200, serviceDiscoveryResult); - httpMock - .scope(secondaryUrl) - .get('/terraform-provider-azurerm/index.json') - .reply(200, {}); - expect( - await getPkgReleases({ - datasource: TerraformProviderDatasource.id, - packageName: 'azurerm', - }), - ).toBeNull(); - }); + it.each` + description | variant + ${'empty results'} | ${'empty'} + ${'404 responses'} | ${'404'} + ${'unknown errors'} | ${'error'} + `( + 'returns null when both default registries return $description', + async ({ variant }) => { + mockDefaultRegistryLookup(variant); - it('returns null for 404', async () => { - httpMock - .scope(primaryUrl) - .get('/v1/providers/hashicorp/azurerm') - .reply(404) - .get('/.well-known/terraform.json') - .reply(200, serviceDiscoveryResult); - httpMock - .scope(secondaryUrl) - .get('/terraform-provider-azurerm/index.json') - .reply(404); - expect( - await getPkgReleases({ - datasource: TerraformProviderDatasource.id, - packageName: 'azurerm', - }), - ).toBeNull(); - }); - - it('returns null for unknown error', async () => { - httpMock - .scope(primaryUrl) - .get('/v1/providers/hashicorp/azurerm') - .replyWithError('') - .get('/.well-known/terraform.json') - .reply(200, serviceDiscoveryResult); - httpMock - .scope(secondaryUrl) - .get('/terraform-provider-azurerm/index.json') - .replyWithError(''); - expect( - await getPkgReleases({ - datasource: TerraformProviderDatasource.id, - packageName: 'azurerm', - }), - ).toBeNull(); - }); + expect( + await getPkgReleases({ + datasource: TerraformProviderDatasource.id, + packageName: 'azurerm', + }), + ).toBeNull(); + }, + ); it('processes real data', async () => { httpMock @@ -105,53 +118,49 @@ describe('modules/datasource/terraform-provider/index', () => { }); }); - it('returns null for empty result from third party', async () => { + it('does not set releaseTimestamp when latest version is not in releases', async () => { httpMock - .scope('https://registry.company.com') - .get('/v1/providers/hashicorp/azurerm/versions') - .reply(200, {}) + .scope(primaryUrl) + .get('/v1/providers/hashicorp/azurerm') + .reply(200, { + source: 'https://github.com/hashicorp/terraform-provider-azurerm', + versions: ['2.52.0'], + version: '9.9.9', + published_at: '2019-11-26T08:22:56Z', + }) .get('/.well-known/terraform.json') .reply(200, serviceDiscoveryResult); - expect( - await getPkgReleases({ - datasource: TerraformProviderDatasource.id, - packageName: 'azurerm', - registryUrls: ['https://registry.company.com'], - }), - ).toBeNull(); + const res = await getPkgReleases({ + datasource: TerraformProviderDatasource.id, + packageName: 'azurerm', + }); + expect(res).toEqual({ + homepage: 'https://registry.terraform.io/providers/hashicorp/azurerm', + registryUrl: 'https://registry.terraform.io', + releases: [{ version: '2.52.0' }], + sourceUrl: 'https://github.com/hashicorp/terraform-provider-azurerm', + }); }); - it('returns null for 404 from third party', async () => { - httpMock - .scope('https://registry.company.com') - .get('/v1/providers/hashicorp/azurerm/versions') - .reply(404) - .get('/.well-known/terraform.json') - .reply(200, serviceDiscoveryResult); - expect( - await getPkgReleases({ - datasource: TerraformProviderDatasource.id, - packageName: 'azurerm', - registryUrls: ['https://registry.company.com'], - }), - ).toBeNull(); - }); + it.each` + description | variant + ${'empty results'} | ${'empty'} + ${'404 responses'} | ${'404'} + ${'unknown errors'} | ${'error'} + `( + 'returns null when a third-party registry returns $description', + async ({ variant }) => { + mockThirdPartyRegistryLookup(variant); - it('returns null for unknown error from third party', async () => { - httpMock - .scope('https://registry.company.com') - .get('/v1/providers/hashicorp/azurerm/versions') - .replyWithError('') - .get('/.well-known/terraform.json') - .reply(200, serviceDiscoveryResult); - expect( - await getPkgReleases({ - datasource: TerraformProviderDatasource.id, - packageName: 'azurerm', - registryUrls: ['https://registry.company.com'], - }), - ).toBeNull(); - }); + expect( + await getPkgReleases({ + datasource: TerraformProviderDatasource.id, + packageName: 'azurerm', + registryUrls: ['https://registry.company.com'], + }), + ).toBeNull(); + }, + ); it('processes real data from third party', async () => { httpMock @@ -216,41 +225,6 @@ describe('modules/datasource/terraform-provider/index', () => { 'https://github.com/terraform-providers/terraform-provider-google-beta', }); }); - - it('simulate failing secondary release source', async () => { - httpMock - .scope(primaryUrl) - .get('/v1/providers/hashicorp/datadog') - .reply(404, { - errors: ['Not Found'], - }) - .get('/.well-known/terraform.json') - .reply(200, serviceDiscoveryResult); - httpMock - .scope(secondaryUrl) - .get('/terraform-provider-datadog/index.json') - .reply(404); - - const res = await getPkgReleases({ - datasource: TerraformProviderDatasource.id, - packageName: 'datadog', - }); - expect(res).toBeNull(); - }); - - it('returns null for error in service discovery', async () => { - httpMock.scope(primaryUrl).get('/.well-known/terraform.json').reply(404); - httpMock - .scope(secondaryUrl) - .get('/terraform-provider-azurerm/index.json') - .replyWithError(''); - expect( - await getPkgReleases({ - datasource: TerraformProviderDatasource.id, - packageName: 'azurerm', - }), - ).toBeNull(); - }); }); describe('getBuilds', () => { diff --git a/lib/modules/datasource/terraform-provider/index.ts b/lib/modules/datasource/terraform-provider/index.ts index dadd587933..5a68c4ad29 100644 --- a/lib/modules/datasource/terraform-provider/index.ts +++ b/lib/modules/datasource/terraform-provider/index.ts @@ -24,9 +24,12 @@ import type { export class TerraformProviderDatasource extends TerraformDatasource { static override readonly id = 'terraform-provider'; + static readonly hashicorpReleaseUrl = 'https://releases.hashicorp.com'; + static readonly terraformRegistryUrl = 'https://registry.terraform.io'; + static readonly defaultRegistryUrls = [ - 'https://registry.terraform.io', - 'https://releases.hashicorp.com', + TerraformProviderDatasource.terraformRegistryUrl, + TerraformProviderDatasource.hashicorpReleaseUrl, ]; static repositoryRegex = regEx(/^hashicorp\/(?\S+)$/); @@ -61,28 +64,24 @@ export class TerraformProviderDatasource extends TerraformDatasource { `terraform-provider.getDependencies() packageName: ${packageName}`, ); - if (registryUrl === this.defaultRegistryUrls[1]) { - return await this.queryReleaseBackend(packageName, registryUrl); - } - const repository = TerraformProviderDatasource.getRepository({ - packageName, - }); - const serviceDiscovery = - await this.getTerraformServiceDiscoveryResult(registryUrl); - - if (registryUrl === this.defaultRegistryUrls[0]) { - return await this.queryRegistryExtendedApi( + if (registryUrl === TerraformProviderDatasource.terraformRegistryUrl) { + const repository = TerraformProviderDatasource.getRepository({ + packageName, + }); + const serviceDiscovery = + await this.getTerraformServiceDiscoveryResult(registryUrl); + return await this.queryTerraformRegistry( serviceDiscovery, registryUrl, repository, ); } + if (registryUrl === TerraformProviderDatasource.hashicorpReleaseUrl) { + return await this.queryReleaseBackend(packageName, registryUrl); + } - return await this.queryRegistryVersions( - serviceDiscovery, - registryUrl, - repository, - ); + // Fall back to the standard Provider Registry Protocol for other registries. + return await this.queryProviderRegistry(registryUrl, packageName); } getReleases(config: GetReleasesConfig): Promise { @@ -103,11 +102,11 @@ export class TerraformProviderDatasource extends TerraformDatasource { } /** - * this uses the api that terraform registry has in addition to the base api - * this endpoint provides more information, such as release date - * this api is undocumented. + * Query the Terraform Registry using the undocumented extended provider API. + * This provides more information than the base Provider Registry Protocol, + * such as the release date for the latest version and the source URL. */ - private async queryRegistryExtendedApi( + private async queryTerraformRegistry( serviceDiscovery: ServiceDiscoveryResult, registryUrl: string, repository: string, @@ -141,14 +140,19 @@ export class TerraformProviderDatasource extends TerraformDatasource { } /** - * this version uses the Provider Registry Protocol that all registries are required to implement + * Query a registry using the Provider Registry Protocol that all registries + * are required to implement. * https://www.terraform.io/internals/provider-registry-protocol */ - private async queryRegistryVersions( - serviceDiscovery: ServiceDiscoveryResult, + private async queryProviderRegistry( registryUrl: string, - repository: string, + packageName: string, ): Promise { + const repository = TerraformProviderDatasource.getRepository({ + packageName, + }); + const serviceDiscovery: ServiceDiscoveryResult = + await this.getTerraformServiceDiscoveryResult(registryUrl); const backendURL = createSDBackendURL( registryUrl, 'providers.v1', @@ -200,7 +204,7 @@ export class TerraformProviderDatasource extends TerraformDatasource { repository: string, version: string, ): Promise { - if (registryURL === TerraformProviderDatasource.defaultRegistryUrls[1]) { + if (registryURL === TerraformProviderDatasource.hashicorpReleaseUrl) { // check if registryURL === secondary backend const repositoryRegexResult = TerraformProviderDatasource.repositoryRegex.exec(repository)?.groups; @@ -356,7 +360,7 @@ export class TerraformProviderDatasource extends TerraformDatasource { ): Promise { return ( await this.http.getJsonUnchecked( - `${TerraformProviderDatasource.defaultRegistryUrls[1]}/${backendLookUpName}/${version}/index.json`, + `${TerraformProviderDatasource.hashicorpReleaseUrl}/${backendLookUpName}/${version}/index.json`, ) ).body; }