Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion projects/api/src/contracts/oauth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export const oauth = builder
`Construct then redirect to this URL. The Trakt website will request permissions for your app, which the user needs to approve. If the user isn't signed into Trakt, it will ask them to do so.

> ### Important
> _Use the website **https://trakt.tv** hostname when creating this URL and not the API URL._
> _Use the **https://auth.trakt.tv** hostname for all OAuth requests, including authorization, device authentication, token exchange and refresh, and token revocation. Do not use the API hostname for these requests._

#### Optional URL Parameters

Expand Down
60 changes: 60 additions & 0 deletions projects/openapi/generate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { OperationObject } from 'openapi3-ts';
import { generate } from './generate.ts';

function assertEquals(actual: unknown, expected: unknown): void {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(
`Expected ${JSON.stringify(expected)}, received ${
JSON.stringify(actual)
}`,
);
}
}

function getOperations(): OperationObject[] {
return Object.values(generate().paths).flatMap((path) =>
Object.values(path ?? {}).filter((value): value is OperationObject =>
typeof value === 'object' && value !== null && 'responses' in value
)
);
}

Deno.test('uses the API host by default', () => {
const document = generate();
const apiOperation = document.paths['/movies/trending']?.get;

assertEquals(document.servers?.[0]?.url, 'https://api.trakt.tv');
assertEquals(apiOperation?.servers, undefined);
});

Deno.test('uses the auth host for every OAuth endpoint', () => {
const oauthOperations = getOperations().filter((operation) =>
operation.tags?.includes('oauth')
);

assertEquals(oauthOperations.length > 0, true);
for (const operation of oauthOperations) {
assertEquals(operation.servers, [{
url: 'https://auth.trakt.tv',
description: 'Authentication',
}]);
}
});

Deno.test('uses the auth host for the OAuth2 flow URLs', () => {
const document = generate();
const oauth2 = document.components?.securitySchemes?.traktOAuth;

if (!oauth2 || !('flows' in oauth2) || !oauth2.flows) {
throw new Error('Expected the traktOAuth OAuth2 security scheme');
}

assertEquals(
oauth2.flows.authorizationCode?.authorizationUrl,
'https://auth.trakt.tv/oauth/authorize',
);
assertEquals(
oauth2.flows.authorizationCode?.tokenUrl,
'https://auth.trakt.tv/oauth/token',
);
});
17 changes: 13 additions & 4 deletions projects/openapi/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function getSecurityFromAuth(
}
}

const servers = [
const apiServers = [
{
url: Environment.production,
description: 'Production',
Expand All @@ -55,6 +55,13 @@ const servers = [
},
];

const authServers = [
{
url: 'https://auth.trakt.tv',
description: 'Authentication',
},
];

export function generate(): ReturnType<typeof generateOpenApi> {
return generateOpenApi(
traktContract,
Expand All @@ -63,7 +70,7 @@ export function generate(): ReturnType<typeof generateOpenApi> {
title: 'Trakt API',
version: '2.0.0',
},
servers,
servers: apiServers,
components: {
securitySchemes: {
traktAPI: {
Expand All @@ -75,8 +82,8 @@ export function generate(): ReturnType<typeof generateOpenApi> {
type: 'oauth2',
flows: {
authorizationCode: {
authorizationUrl: 'https://trakt.tv/oauth/authorize',
tokenUrl: 'https://api.trakt.tv/oauth/token',
authorizationUrl: 'https://auth.trakt.tv/oauth/authorize',
tokenUrl: 'https://auth.trakt.tv/oauth/token',
scopes: {
public: 'Access public data',
},
Expand Down Expand Up @@ -106,10 +113,12 @@ export function generate(): ReturnType<typeof generateOpenApi> {
];

const auth = getAuthRequirement(route.metadata, operation.tags ?? []);
const isAuthEndpoint = operation.tags?.includes('oauth') ?? false;

return {
...operation,
security: getSecurityFromAuth(auth),
...(isAuthEndpoint ? { servers: authServers } : {}),
operationId: `${route.method.toLowerCase()}${
parts
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
Expand Down
Loading