diff --git a/package.json b/package.json index e65a119..6a79f8d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint:fix": "eslint --fix", "test": "npm run test:unit && npm run test:typescript", "test:coverage": "npm run test:unit -- --cov --coverage-report=html", - "test:typescript": "tsd", + "test:typescript": "tstyche", "test:unit": "c8 --100 node --test" }, "repository": { @@ -65,7 +65,7 @@ "fastify": "^5.6.2", "neostandard": "^0.13.0", "nock": "^14.0.10", - "tsd": "^0.33.0" + "tstyche": "^7.0.0" }, "dependencies": { "@fastify/cookie": "^11.0.2", @@ -75,4 +75,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/types/index.test-d.ts b/types/index.test-d.ts deleted file mode 100644 index 332f387..0000000 --- a/types/index.test-d.ts +++ /dev/null @@ -1,302 +0,0 @@ -import fastify, { FastifyInstance } from 'fastify' -import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd' -import fastifyOauth2, { - FastifyOAuth2Options, - Credentials, - OAuth2Namespace, - OAuth2Token, - ProviderConfiguration, - UserInfoExtraOptions -} from '..' -import type { ModuleOptions } from 'simple-oauth2' - -/** - * Preparing some data for testing. - */ -const auth = fastifyOauth2.GOOGLE_CONFIGURATION -const scope = ['r_emailaddress', 'r_basicprofile'] -const tags = ['oauth2', 'oauth'] -const credentials: Credentials = { - client: { - id: 'test_id', - secret: 'test_secret', - }, - auth, -} - -const simpleOauth2Options: ModuleOptions = { - client: { - id: 'test_id', - secret: 'test_secret', - }, - auth, -} - -const OAuth2NoneOptional: FastifyOAuth2Options = { - name: 'testOAuthName', - credentials, - callbackUri: 'http://localhost/testOauth/callback' -} - -const OAuth2Options: FastifyOAuth2Options = { - name: 'testOAuthName', - scope, - credentials, - callbackUri: 'http://localhost/testOauth/callback', - callbackUriParams: {}, - generateStateFunction: function () { - expectType(this) - return 'test' - }, - checkStateFunction: function () { - expectType(this) - return true - }, - startRedirectPath: '/login/testOauth', - cookie: { - secure: true, - sameSite: 'none' - }, - redirectStateCookieName: 'redirect-state-cookie', - verifierCookieName: 'verifier-cookie', -} - -expectAssignable({ - name: 'testOAuthName', - scope, - credentials, - callbackUri: 'http://localhost/testOauth/callback', - callbackUriParams: {}, - startRedirectPath: '/login/testOauth', - pkce: 'S256' -}) - -expectAssignable({ - name: 'testOAuthName', - scope, - credentials, - callbackUri: req => `${req.protocol}://${req.hostname}/callback`, - callbackUriParams: {}, - startRedirectPath: '/login/testOauth', - pkce: 'S256' -}) - -expectAssignable({ - name: 'testOAuthName', - scope, - credentials, - callbackUri: 'http://localhost/testOauth/callback', - callbackUriParams: {}, - startRedirectPath: '/login/testOauth', - discovery: { issuer: 'https://idp.mycompany.com' } -}) - -expectNotAssignable({ - name: 'testOAuthName', - scope, - credentials, - callbackUri: 'http://localhost/testOauth/callback', - callbackUriParams: {}, - startRedirectPath: '/login/testOauth', - discovery: { issuer: 1 } -}) - -expectAssignable({ - name: 'testOAuthName', - scope, - credentials, - callbackUri: 'http://localhost/testOauth/callback', - callbackUriParams: {}, - startRedirectPath: '/login/testOauth', - pkce: 'plain' -}) - -expectNotAssignable({ - name: 'testOAuthName', - scope, - credentials, - callbackUri: 'http://localhost/testOauth/callback', - callbackUriParams: {}, - generateStateFunction: () => { - }, - checkStateFunction: () => { - }, - startRedirectPath: '/login/testOauth', - pkce: 'SOMETHING' -}) - -const server = fastify() - -server.register(fastifyOauth2, OAuth2NoneOptional) -server.register(fastifyOauth2, OAuth2Options) - -server.register(fastifyOauth2, { - name: 'testOAuthName', - scope, - credentials, - callbackUri: 'http://localhost/testOauth/callback', - checkStateFunction: () => true, -}) - -expectError(server.register(fastifyOauth2, { - name: 'testOAuthName', - scope, - credentials, - callbackUri: 'http://localhost/testOauth/callback', - checkStateFunction: () => true, - startRedirectPath: 2, -})) - -declare module 'fastify' { - // Developers need to define this in their code like they have to do with all decorators. - interface FastifyInstance { - testOAuthName: OAuth2Namespace; - } -} - -/** - * Actual testing. - */ -expectType(auth) -expectType(scope) -expectType(tags) -expectType(credentials) - -// Ensure duplicayed simple-oauth2 are compatible with simple-oauth2 -expectAssignable>({ auth: { tokenHost: '' }, ...credentials }) -expectAssignable(auth) -// Ensure published types of simple-oauth2 are accepted -expectAssignable(simpleOauth2Options) -expectAssignable(simpleOauth2Options.auth) - -expectError(fastifyOauth2()) // error because missing required arguments -expectError(fastifyOauth2(server, {}, () => { -})) // error because missing required options - -expectAssignable(fastifyOauth2.DISCORD_CONFIGURATION) -expectAssignable(fastifyOauth2.FACEBOOK_CONFIGURATION) -expectAssignable(fastifyOauth2.GITHUB_CONFIGURATION) -expectAssignable(fastifyOauth2.GITLAB_CONFIGURATION) -expectAssignable(fastifyOauth2.GOOGLE_CONFIGURATION) -expectAssignable(fastifyOauth2.LINKEDIN_CONFIGURATION) -expectAssignable(fastifyOauth2.MICROSOFT_CONFIGURATION) -expectAssignable(fastifyOauth2.SPOTIFY_CONFIGURATION) -expectAssignable(fastifyOauth2.VKONTAKTE_CONFIGURATION) -expectAssignable(fastifyOauth2.TWITCH_CONFIGURATION) -expectAssignable(fastifyOauth2.VATSIM_CONFIGURATION) -expectAssignable(fastifyOauth2.VATSIM_DEV_CONFIGURATION) -expectAssignable(fastifyOauth2.EPIC_GAMES_CONFIGURATION) -expectAssignable(fastifyOauth2.YANDEX_CONFIGURATION) - -server.get('/testOauth/callback', async (request, reply) => { - expectType(server.testOAuthName) - expectType(server.oauth2TestOAuthName) - - expectType(await server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request)) - expectType>(server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request)) - - expectType(await server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request, reply)) - expectType>(server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request, reply)) - expectType( - server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request, (_err: any, _t: OAuth2Token): void => { - }) - ) - expectType( - server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request, reply, (_err: any, _t: OAuth2Token): void => { - }) - ) - // error because Promise should not return void - expectError(await server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request)) - // error because non-Promise function call should return void and have a callback argument - expectError( - server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request, (_err: any, _t: OAuth2Token): void => { - }) - ) - - // error because function call does not pass a callback as second argument. - expectError(server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request)) - - const token = await server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request) - if (token.token.refresh_token) { - expectType( - await server.testOAuthName.getNewAccessTokenUsingRefreshToken(token.token, {}) - ) - expectType>( - server.testOAuthName.getNewAccessTokenUsingRefreshToken(token.token, {}) - ) - expectType( - server.testOAuthName.getNewAccessTokenUsingRefreshToken( - token.token, - {}, - (_err: any, _t: OAuth2Token): void => { - } - ) - ) - // Expect error because Promise should not return void - expectError(server.testOAuthName.revokeToken(token.token, 'access_token', undefined)) - // Correct way - expectType>(server.testOAuthName.revokeToken(token.token, 'access_token', undefined)) - // Expect error because invalid Type test isn't an access_token or refresh_token - expectError>(server.testOAuthName.revokeToken(token.token, 'test', undefined)) - // Correct way - expectType( - server.testOAuthName.revokeToken(token.token, 'refresh_token', undefined, (_err: any): void => { - }) - ) - // Expect error because invalid Type test isn't an access_token or refresh_token - expectError( - server.testOAuthName.revokeToken(token.token, 'test', undefined, (_err: any): void => { - }) - ) - // Expect error because invalid Type test isn't an access_token or refresh_token - expectError( - server.testOAuthName.revokeToken(token.token, 'access_token', undefined, undefined) - ) - - // Expect error because Promise should not return void - expectError(server.testOAuthName.revokeAllToken(token.token, undefined)) - // Correct way - expectType>(server.testOAuthName.revokeAllToken(token.token, undefined)) - // Correct way too - expectType(server.testOAuthName.revokeAllToken(token.token, undefined, (_err: any): void => { - })) - // Invalid content - expectError(server.testOAuthName.revokeAllToken(token.token, undefined, undefined)) - // error because Promise should not return void - expectError(await server.testOAuthName.getNewAccessTokenUsingRefreshToken(token.token, {})) - // error because non-Promise function call should return void and have a callback argument - expectError( - server.testOAuthName.getNewAccessTokenUsingRefreshToken( - token.token, - {}, - (_err: any, _t: OAuth2Token): void => { - } - ) - ) - // error because function call does not pass a callback as second argument. - expectError(server.testOAuthName.getNewAccessTokenUsingRefreshToken(token.token, {})) - } - - expectType>(server.testOAuthName.generateAuthorizationUri(request, reply)) - expectType(server.testOAuthName.generateAuthorizationUri(request, reply, (_err) => {})) - // BEGIN userinfo tests - expectType>(server.testOAuthName.userinfo(token.token)) - expectType>(server.testOAuthName.userinfo(token.token.access_token)) - expectType(await server.testOAuthName.userinfo(token.token.access_token)) - expectType(server.testOAuthName.userinfo(token.token.access_token, () => {})) - expectType(server.testOAuthName.userinfo(token.token.access_token, undefined, () => {})) - expectAssignable({ method: 'GET', params: {}, via: 'header' }) - expectAssignable({ method: 'POST', params: { a: 1 }, via: 'header' }) - expectAssignable({ via: 'body' }) - expectNotAssignable({ via: 'donkey' }) - expectNotAssignable({ something: 1 }) - // END userinfo tests - - expectType(await server.testOAuthName.generateAuthorizationUri(request, reply)) - // error because missing reply argument - expectError(server.testOAuthName.generateAuthorizationUri(request)) - - return { - access_token: token.token.access_token, - } -}) diff --git a/types/index.tst.ts b/types/index.tst.ts new file mode 100644 index 0000000..41ca5d1 --- /dev/null +++ b/types/index.tst.ts @@ -0,0 +1,314 @@ +import fastify, { FastifyInstance, FastifyRequest } from 'fastify' +import { expect } from 'tstyche' +import fastifyOauth2, { + FastifyOAuth2Options, + Credentials, + OAuth2Namespace, + OAuth2Token, + ProviderConfiguration, + UserInfoExtraOptions +} from '.' +import type { ModuleOptions } from 'simple-oauth2' + +/** + * Preparing some data for testing. + */ +const auth = fastifyOauth2.GOOGLE_CONFIGURATION +const scope = ['r_emailaddress', 'r_basicprofile'] +const tags = ['oauth2', 'oauth'] +const credentials: Credentials = { + client: { + id: 'test_id', + secret: 'test_secret', + }, + auth, +} + +const simpleOauth2Options: ModuleOptions = { + client: { + id: 'test_id', + secret: 'test_secret', + }, + auth, +} + +const OAuth2NoneOptional: FastifyOAuth2Options = { + name: 'testOAuthName', + credentials, + callbackUri: 'http://localhost/testOauth/callback' +} + +const OAuth2Options: FastifyOAuth2Options = { + name: 'testOAuthName', + scope, + credentials, + callbackUri: 'http://localhost/testOauth/callback', + callbackUriParams: {}, + generateStateFunction: function () { + expect(this).type.toBe() + return 'test' + }, + checkStateFunction: function () { + expect(this).type.toBe() + return true + }, + startRedirectPath: '/login/testOauth', + cookie: { + secure: true, + sameSite: 'none' + }, + redirectStateCookieName: 'redirect-state-cookie', + verifierCookieName: 'verifier-cookie', +} + +expect({ + name: 'testOAuthName', + scope, + credentials, + callbackUri: 'http://localhost/testOauth/callback', + callbackUriParams: {}, + startRedirectPath: '/login/testOauth', + pkce: 'S256' +} as const).type.toBeAssignableTo() + +expect({ + name: 'testOAuthName', + scope, + credentials, + callbackUri: (req: FastifyRequest) => `${req.protocol}://${req.hostname}/callback`, + callbackUriParams: {}, + startRedirectPath: '/login/testOauth', + pkce: 'S256' +} as const).type.toBeAssignableTo() + +expect({ + name: 'testOAuthName', + scope, + credentials, + callbackUri: 'http://localhost/testOauth/callback', + callbackUriParams: {}, + startRedirectPath: '/login/testOauth', + discovery: { issuer: 'https://idp.mycompany.com' } +} as const).type.toBeAssignableTo() + +expect({ + name: 'testOAuthName', + scope, + credentials, + callbackUri: 'http://localhost/testOauth/callback', + callbackUriParams: {}, + startRedirectPath: '/login/testOauth', + discovery: { issuer: 1 } +}).type.not.toBeAssignableTo() + +expect({ + name: 'testOAuthName', + scope, + credentials, + callbackUri: 'http://localhost/testOauth/callback', + callbackUriParams: {}, + startRedirectPath: '/login/testOauth', + pkce: 'plain' +} as const).type.toBeAssignableTo() + +expect({ + name: 'testOAuthName', + scope, + credentials, + callbackUri: 'http://localhost/testOauth/callback', + callbackUriParams: {}, + generateStateFunction: () => { + }, + checkStateFunction: () => { + }, + startRedirectPath: '/login/testOauth', + pkce: 'SOMETHING' +}).type.not.toBeAssignableTo() + +const server = fastify() + +server.register(fastifyOauth2, OAuth2NoneOptional) +server.register(fastifyOauth2, OAuth2Options) + +server.register(fastifyOauth2, { + name: 'testOAuthName', + scope, + credentials, + callbackUri: 'http://localhost/testOauth/callback', + checkStateFunction: () => true, +}) + +// @ts-expect-error! +server.register(fastifyOauth2, { + name: 'testOAuthName', + scope, + credentials, + callbackUri: 'http://localhost/testOauth/callback', + checkStateFunction: () => true, + startRedirectPath: 2, +}) + +declare module 'fastify' { + interface FastifyInstance { + testOAuthName: OAuth2Namespace; + } +} + +/** + * Actual testing. + */ +expect(auth).type.toBe() +expect(scope).type.toBe() +expect(tags).type.toBe() +expect(credentials).type.toBe() + +// Ensure duplicated simple-oauth2 are compatible with simple-oauth2 +expect({ auth: { tokenHost: '' }, ...credentials }).type.toBeAssignableTo>() +expect(auth).type.toBeAssignableTo() + +// Ensure published types of simple-oauth2 are accepted +expect(simpleOauth2Options).type.toBeAssignableTo() +expect(simpleOauth2Options.auth).type.toBeAssignableTo() + +// @ts-expect-error! +fastifyOauth2() + +// @ts-expect-error! +fastifyOauth2(server, {}, () => {}) + +expect(fastifyOauth2.DISCORD_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.FACEBOOK_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.GITHUB_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.GITLAB_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.GOOGLE_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.LINKEDIN_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.MICROSOFT_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.SPOTIFY_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.VKONTAKTE_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.TWITCH_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.VATSIM_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.VATSIM_DEV_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.EPIC_GAMES_CONFIGURATION).type.toBeAssignableTo() +expect(fastifyOauth2.YANDEX_CONFIGURATION).type.toBeAssignableTo() + +server.get('/testOauth/callback', async (request, reply) => { + expect(server.testOAuthName).type.toBe() + expect(server.oauth2TestOAuthName).type.toBe() + + expect(await server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request)).type.toBe() + expect(server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request)).type.toBe>() + + expect(await server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request, reply)).type.toBe() + expect(server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request, reply)).type.toBe>() + + expect( + server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request, (_err: any, _t: OAuth2Token): void => { + }) + ).type.toBe() + + expect( + server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request, reply, (_err: any, _t: OAuth2Token): void => { + }) + ).type.toBe() + + expect( + await server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request) + ).type.not.toBe() + + expect( + server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow( + request, + (_err: any, _t: OAuth2Token): void => {} + ) + ).type.not.toBe() + + expect(server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request)).type.not.toBe() + + const token = await server.testOAuthName.getAccessTokenFromAuthorizationCodeFlow(request) + if (token.token.refresh_token) { + expect( + await server.testOAuthName.getNewAccessTokenUsingRefreshToken(token.token, {}) + ).type.toBe() + + expect( + server.testOAuthName.getNewAccessTokenUsingRefreshToken(token.token, {}) + ).type.toBe>() + + expect( + server.testOAuthName.getNewAccessTokenUsingRefreshToken( + token.token, + {}, + (_err: any, _t: OAuth2Token): void => { + } + ) + ).type.toBe() + + expect(server.testOAuthName.revokeToken(token.token, 'access_token', undefined)).type.not.toBe() + + expect(server.testOAuthName.revokeToken(token.token, 'access_token', undefined)).type.toBe>() + + // @ts-expect-error! + server.testOAuthName.revokeToken(token.token, 'test', undefined) + + expect( + server.testOAuthName.revokeToken(token.token, 'refresh_token', undefined, (_err: any): void => { + }) + ).type.toBe() + + // @ts-expect-error! + server.testOAuthName.revokeToken(token.token, 'test', undefined, (_err: any): void => {}) + + // @ts-expect-error! + server.testOAuthName.revokeToken(token.token, 'access_token', undefined, undefined) + + expect(server.testOAuthName.revokeAllToken(token.token, undefined)).type.not.toBe() + + expect(server.testOAuthName.revokeAllToken(token.token, undefined)).type.toBe>() + + expect(server.testOAuthName.revokeAllToken(token.token, undefined, (_err: any): void => { + })).type.toBe() + + // @ts-expect-error! + server.testOAuthName.revokeAllToken(token.token, undefined, undefined) + + expect(await server.testOAuthName.getNewAccessTokenUsingRefreshToken(token.token, {})).type.not.toBe() + + expect( + server.testOAuthName.getNewAccessTokenUsingRefreshToken( + token.token, + {}, + (_err: any, _t: OAuth2Token): void => { + } + ) + ).type.not.toBe() + + expect(server.testOAuthName.getNewAccessTokenUsingRefreshToken(token.token, {})).type.not.toBe() + } + + expect(server.testOAuthName.generateAuthorizationUri(request, reply)).type.toBe>() + expect(server.testOAuthName.generateAuthorizationUri(request, reply, (_err) => {})).type.toBe() + + // BEGIN userinfo tests + expect(server.testOAuthName.userinfo(token.token)).type.toBe>() + expect(server.testOAuthName.userinfo(token.token.access_token)).type.toBe>() + expect(await server.testOAuthName.userinfo(token.token.access_token)).type.toBe() + expect(server.testOAuthName.userinfo(token.token.access_token, () => {})).type.toBe() + expect(server.testOAuthName.userinfo(token.token.access_token, undefined, () => {})).type.toBe() + + expect({ method: 'GET', params: {}, via: 'header' } as const).type.toBeAssignableTo() + expect({ method: 'POST', params: { a: 1 }, via: 'header' } as const).type.toBeAssignableTo() + expect({ via: 'body' } as const).type.toBeAssignableTo() + + expect({ via: 'donkey' }).type.not.toBeAssignableTo() + expect({ something: 1 }).type.not.toBeAssignableTo() + // END userinfo tests + + expect(await server.testOAuthName.generateAuthorizationUri(request, reply)).type.toBe() + + // @ts-expect-error! + server.testOAuthName.generateAuthorizationUri(request) + + return { + access_token: token.token.access_token, + } +})