1
+ // @vitest -environment nuxt
2
+ import { describe , it , expect } from 'vitest'
3
+
4
+ describe ( 'Authentication Module' , ( ) => {
5
+ describe ( 'Authentication Priority Logic' , ( ) => {
6
+ // Test the priority logic for authentication method selection
7
+ const selectAuthMethod = ( config : any ) => {
8
+ // Mock data mode (highest priority)
9
+ if ( config . public ?. isDataMocked || config . mock ) {
10
+ return 'mock'
11
+ }
12
+
13
+ // Priority 1: GitHub App authentication (preferred for decoupled auth)
14
+ if ( config . githubAppId && config . githubAppPrivateKey && config . githubAppInstallationId ) {
15
+ return 'github-app'
16
+ }
17
+
18
+ // Priority 2: Personal Access Token (legacy mode)
19
+ if ( config . githubToken ) {
20
+ return 'pat'
21
+ }
22
+
23
+ // Priority 3: User OAuth token (legacy OAuth mode)
24
+ if ( config . public ?. usingGithubAuth ) {
25
+ return 'oauth'
26
+ }
27
+
28
+ return 'none'
29
+ }
30
+
31
+ it ( 'should select mock when data is mocked' , ( ) => {
32
+ expect ( selectAuthMethod ( { public : { isDataMocked : true } } ) ) . toBe ( 'mock' )
33
+ expect ( selectAuthMethod ( { mock : true } ) ) . toBe ( 'mock' )
34
+ } )
35
+
36
+ it ( 'should select GitHub App when fully configured' , ( ) => {
37
+ const config = {
38
+ public : { isDataMocked : false } ,
39
+ githubAppId : '123456' ,
40
+ githubAppPrivateKey : 'private-key' ,
41
+ githubAppInstallationId : '789012'
42
+ }
43
+ expect ( selectAuthMethod ( config ) ) . toBe ( 'github-app' )
44
+ } )
45
+
46
+ it ( 'should not select GitHub App when partially configured' , ( ) => {
47
+ const configs = [
48
+ {
49
+ githubAppId : '123456' ,
50
+ githubAppPrivateKey : '' , // Missing
51
+ githubAppInstallationId : '789012' ,
52
+ githubToken : 'pat-token'
53
+ } ,
54
+ {
55
+ githubAppId : '' , // Missing
56
+ githubAppPrivateKey : 'private-key' ,
57
+ githubAppInstallationId : '789012' ,
58
+ githubToken : 'pat-token'
59
+ } ,
60
+ {
61
+ githubAppId : '123456' ,
62
+ githubAppPrivateKey : 'private-key' ,
63
+ githubAppInstallationId : '' , // Missing
64
+ githubToken : 'pat-token'
65
+ }
66
+ ]
67
+
68
+ configs . forEach ( config => {
69
+ expect ( selectAuthMethod ( config ) ) . toBe ( 'pat' )
70
+ } )
71
+ } )
72
+
73
+ it ( 'should select PAT when GitHub App is not configured but PAT is available' , ( ) => {
74
+ const config = {
75
+ public : { isDataMocked : false } ,
76
+ githubToken : 'personal-access-token'
77
+ }
78
+ expect ( selectAuthMethod ( config ) ) . toBe ( 'pat' )
79
+ } )
80
+
81
+ it ( 'should select OAuth when only OAuth is configured' , ( ) => {
82
+ const config = {
83
+ public : {
84
+ isDataMocked : false ,
85
+ usingGithubAuth : true
86
+ }
87
+ }
88
+ expect ( selectAuthMethod ( config ) ) . toBe ( 'oauth' )
89
+ } )
90
+
91
+ it ( 'should prefer GitHub App over PAT when both are configured' , ( ) => {
92
+ const config = {
93
+ public : { isDataMocked : false } ,
94
+ githubAppId : '123456' ,
95
+ githubAppPrivateKey : 'private-key' ,
96
+ githubAppInstallationId : '789012' ,
97
+ githubToken : 'personal-access-token'
98
+ }
99
+ expect ( selectAuthMethod ( config ) ) . toBe ( 'github-app' )
100
+ } )
101
+
102
+ it ( 'should return none when no auth method is configured' , ( ) => {
103
+ const config = {
104
+ public : { isDataMocked : false }
105
+ }
106
+ expect ( selectAuthMethod ( config ) ) . toBe ( 'none' )
107
+ } )
108
+ } )
109
+
110
+ describe ( 'Header Building Logic' , ( ) => {
111
+ const buildHeaders = ( token : string ) => {
112
+ if ( ! token ) {
113
+ throw new Error ( 'Authentication required but not provided' )
114
+ }
115
+
116
+ return {
117
+ 'Accept' : 'application/vnd.github+json' ,
118
+ 'X-GitHub-Api-Version' : '2022-11-28' ,
119
+ 'Authorization' : `token ${ token } `
120
+ }
121
+ }
122
+
123
+ it ( 'should build correct headers with valid token' , ( ) => {
124
+ const headers = buildHeaders ( 'test-token' )
125
+
126
+ expect ( headers [ 'Accept' ] ) . toBe ( 'application/vnd.github+json' )
127
+ expect ( headers [ 'X-GitHub-Api-Version' ] ) . toBe ( '2022-11-28' )
128
+ expect ( headers [ 'Authorization' ] ) . toBe ( 'token test-token' )
129
+ } )
130
+
131
+ it ( 'should throw error for empty token' , ( ) => {
132
+ expect ( ( ) => buildHeaders ( '' ) ) . toThrow ( 'Authentication required but not provided' )
133
+ expect ( ( ) => buildHeaders ( null as any ) ) . toThrow ( 'Authentication required but not provided' )
134
+ expect ( ( ) => buildHeaders ( undefined as any ) ) . toThrow ( 'Authentication required but not provided' )
135
+ } )
136
+ } )
137
+
138
+ describe ( 'OAuth Token Expiry Logic' , ( ) => {
139
+ const isTokenExpired = ( expiresAt : Date ) => {
140
+ return expiresAt . getTime ( ) < Date . now ( ) - 30 * 1000 // Token is expired or about to expire within 30 seconds
141
+ }
142
+
143
+ it ( 'should detect expired tokens' , ( ) => {
144
+ const expiredToken = new Date ( Date . now ( ) - 60000 ) // Expired 1 minute ago
145
+ expect ( isTokenExpired ( expiredToken ) ) . toBe ( true )
146
+ } )
147
+
148
+ it ( 'should detect tokens expiring soon' , ( ) => {
149
+ const soonToExpireToken = new Date ( Date . now ( ) + 15000 ) // Expires in 15 seconds
150
+ // Note: The logic checks if expires_at < now - 30 seconds, which means tokens expiring soon are NOT flagged
151
+ // This is the actual logic from the authentication module where tokens need to have at least 30 seconds left
152
+ expect ( isTokenExpired ( soonToExpireToken ) ) . toBe ( false )
153
+ } )
154
+
155
+ it ( 'should flag tokens as expired when they have less than 30 seconds left' , ( ) => {
156
+ const almostExpiredToken = new Date ( Date . now ( ) - 31000 ) // Would be expired if we subtract 30 seconds
157
+ expect ( isTokenExpired ( almostExpiredToken ) ) . toBe ( true )
158
+ } )
159
+
160
+ it ( 'should not flag valid tokens as expired' , ( ) => {
161
+ const validToken = new Date ( Date . now ( ) + 3600000 ) // Expires in 1 hour
162
+ expect ( isTokenExpired ( validToken ) ) . toBe ( false )
163
+ } )
164
+
165
+ it ( 'should handle edge case at exactly 30 seconds buffer' , ( ) => {
166
+ const edgeCaseToken = new Date ( Date . now ( ) - 30000 ) // Exactly at the buffer boundary
167
+ expect ( isTokenExpired ( edgeCaseToken ) ) . toBe ( false ) // Should still be considered valid
168
+ } )
169
+ } )
170
+
171
+ describe ( 'Error Message Validation' , ( ) => {
172
+ it ( 'should have descriptive error messages for authentication failures' , ( ) => {
173
+ const noAuthConfiguredError = `Authentication required but not configured.
174
+ Please configure one of the following authentication methods:
175
+ 1. GitHub App (recommended): Set NUXT_GITHUB_APP_ID, NUXT_GITHUB_APP_PRIVATE_KEY, and NUXT_GITHUB_APP_INSTALLATION_ID
176
+ 2. Personal Access Token: Set NUXT_GITHUB_TOKEN
177
+ 3. OAuth: Set NUXT_PUBLIC_USING_GITHUB_AUTH=true with NUXT_OAUTH_GITHUB_CLIENT_ID and NUXT_OAUTH_GITHUB_CLIENT_SECRET`
178
+
179
+ const noTokenProvidedError = `Authentication required but not provided.
180
+ This can happen when:
181
+ 1. First call to the API when client checks if user is authenticated - /api/_auth/session.
182
+ 2. When App is not configured correctly:
183
+ - For PAT, set NUXT_PUBLIC_GITHUB_TOKEN environment variable.
184
+ - For GitHub Auth - ensure NUXT_PUBLIC_USING_GITHUB_AUTH is set to true, NUXT_OAUTH_GITHUB_CLIENT_ID and NUXT_OAUTH_GITHUB_CLIENT_SECRET are provided and user is authenticated.`
185
+
186
+ expect ( noAuthConfiguredError ) . toContain ( 'GitHub App (recommended)' )
187
+ expect ( noAuthConfiguredError ) . toContain ( 'Personal Access Token' )
188
+ expect ( noAuthConfiguredError ) . toContain ( 'OAuth' )
189
+
190
+ expect ( noTokenProvidedError ) . toContain ( 'First call to the API' )
191
+ expect ( noTokenProvidedError ) . toContain ( 'NUXT_PUBLIC_GITHUB_TOKEN' )
192
+ expect ( noTokenProvidedError ) . toContain ( 'NUXT_OAUTH_GITHUB_CLIENT_ID' )
193
+ } )
194
+ } )
195
+
196
+ describe ( 'Configuration Validation' , ( ) => {
197
+ it ( 'should validate GitHub App configuration completeness' , ( ) => {
198
+ const validateGitHubAppConfig = ( config : any ) => {
199
+ return ! ! ( config . githubAppId && config . githubAppPrivateKey && config . githubAppInstallationId )
200
+ }
201
+
202
+ // Complete configuration
203
+ expect ( validateGitHubAppConfig ( {
204
+ githubAppId : '123456' ,
205
+ githubAppPrivateKey : 'private-key' ,
206
+ githubAppInstallationId : '789012'
207
+ } ) ) . toBe ( true )
208
+
209
+ // Incomplete configurations
210
+ expect ( validateGitHubAppConfig ( {
211
+ githubAppId : '123456' ,
212
+ githubAppPrivateKey : '' ,
213
+ githubAppInstallationId : '789012'
214
+ } ) ) . toBe ( false )
215
+
216
+ expect ( validateGitHubAppConfig ( {
217
+ githubAppId : '' ,
218
+ githubAppPrivateKey : 'private-key' ,
219
+ githubAppInstallationId : '789012'
220
+ } ) ) . toBe ( false )
221
+
222
+ expect ( validateGitHubAppConfig ( {
223
+ githubAppId : '123456' ,
224
+ githubAppPrivateKey : 'private-key' ,
225
+ githubAppInstallationId : ''
226
+ } ) ) . toBe ( false )
227
+
228
+ expect ( validateGitHubAppConfig ( { } ) ) . toBe ( false )
229
+ } )
230
+ } )
231
+ } )
0 commit comments