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
This commit is contained in:
Arnaud Dezandee
2026-04-03 08:19:13 +02:00
committed by GitHub
parent 1a3cae8b3d
commit 59274d2349
2 changed files with 137 additions and 159 deletions
@@ -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', () => {
@@ -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\/(?<packageName>\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<ReleaseResult | null> {
@@ -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<ReleaseResult> {
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<TerraformBuild[] | null> {
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<VersionDetailResponse> {
return (
await this.http.getJsonUnchecked<VersionDetailResponse>(
`${TerraformProviderDatasource.defaultRegistryUrls[1]}/${backendLookUpName}/${version}/index.json`,
`${TerraformProviderDatasource.hashicorpReleaseUrl}/${backendLookUpName}/${version}/index.json`,
)
).body;
}