1
1
import { describe , it , expect , jest , beforeAll , afterAll , beforeEach } from "@jest/globals" ;
2
- import { fetchGitHubFile } from "src/util/github.js" ;
3
- import { GitHubFileResponse , GitHubInstance } from "src/model/github.js" ;
2
+ import { fetchGitHubFile , GitHubContentItem } from "src/util/github.js" ;
3
+ import { GitHubInstance } from "src/model/github.js" ;
4
4
import { GitHubFileNotFoundError , GitHubNetworkError , GitHubAccessError } from "src/model/error/GithubErrors.js" ;
5
+ import { Buffer } from "buffer" ;
5
6
6
- describe ( "GitHub" , ( ) => {
7
+ describe ( "GitHub Util " , ( ) => {
7
8
beforeAll ( ( ) => {
8
9
const mockFetch = jest . fn ( ) as jest . MockedFunction < typeof fetch > ;
9
10
global . fetch = mockFetch ;
@@ -23,114 +24,172 @@ describe("GitHub", () => {
23
24
branch : "main" ,
24
25
} ;
25
26
26
- it ( "should fetch GitHub file successfully" , async ( ) => {
27
- const mockFileContent : GitHubFileResponse = {
27
+ const mockToken = "test-token" ;
28
+ const fileContent = "Decoded file content" ;
29
+ const fileContentBase64 = Buffer . from ( fileContent ) . toString ( "base64" ) ;
30
+ const filePath = "path/to/test.json" ;
31
+ const fileSha = "abc123def456" ;
32
+ const contentsUrl = `https://api.github.com/repos/owner/repo/contents/${ filePath } ?ref=main` ;
33
+ const blobUrl = `https://api.github.com/repos/owner/repo/git/blobs/${ fileSha } ` ;
34
+
35
+ it ( "should fetch small GitHub file successfully (content in first response)" , async ( ) => {
36
+ const mockMetadataResponse : GitHubContentItem = {
28
37
name : "test.json" ,
29
- path : "test.json" ,
30
- sha : "abc123" ,
38
+ path : filePath ,
39
+ sha : fileSha ,
31
40
size : 100 ,
32
- url : "https://api.github.com/repos/owner/repo/contents/test.json" ,
33
- html_url : "https://github.com/owner/repo/blob/main/test.json" ,
34
- git_url : "https://api.github.com/repos/owner/repo/git/blobs/abc123" ,
35
- download_url : "https://raw.githubusercontent.com/owner/repo/main/test.json" ,
36
41
type : "file" ,
37
- content : "base64encodedcontent" ,
42
+ content : fileContentBase64 ,
38
43
encoding : "base64" ,
39
44
} ;
40
- const mockResponse : Partial < Response > = {
45
+ const mockFetchResponse : Partial < Response > = {
41
46
ok : true ,
42
47
status : 200 ,
43
- statusText : "OK" ,
44
- json : jest . fn < ( ) => Promise < unknown > > ( ) . mockResolvedValue ( mockFileContent ) ,
48
+ json : jest . fn < ( ) => Promise < unknown > > ( ) . mockResolvedValue ( mockMetadataResponse ) ,
49
+ } ;
50
+
51
+ const mockFetch = global . fetch as jest . MockedFunction < typeof fetch > ;
52
+ mockFetch . mockResolvedValueOnce ( mockFetchResponse as Response ) ;
53
+
54
+ const result = await fetchGitHubFile ( mockInstance , filePath , mockToken ) ;
55
+
56
+ expect ( result ) . toEqual ( fileContent ) ;
57
+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 1 ) ;
58
+ expect ( mockFetch ) . toHaveBeenCalledWith ( contentsUrl , {
59
+ headers : { Authorization : `Token ${ mockToken } ` } ,
60
+ } ) ;
61
+ } ) ;
62
+
63
+ it ( "should fetch large GitHub file successfully (content via blob API)" , async ( ) => {
64
+ const mockMetadataResponse : GitHubContentItem = {
65
+ name : "test.json" ,
66
+ path : filePath ,
67
+ sha : fileSha ,
68
+ size : 2 * 1024 * 1024 ,
69
+ type : "file" ,
70
+ } ;
71
+ const mockBlobResponseData = {
72
+ sha : fileSha ,
73
+ size : 2 * 1024 * 1024 ,
74
+ content : fileContentBase64 ,
75
+ encoding : "base64" ,
76
+ } ;
77
+
78
+ const mockFetchMetadataResponse : Partial < Response > = {
79
+ ok : true ,
80
+ status : 200 ,
81
+ json : jest . fn < ( ) => Promise < unknown > > ( ) . mockResolvedValue ( mockMetadataResponse ) ,
82
+ } ;
83
+ const mockFetchBlobResponse : Partial < Response > = {
84
+ ok : true ,
85
+ status : 200 ,
86
+ json : jest . fn < ( ) => Promise < unknown > > ( ) . mockResolvedValue ( mockBlobResponseData ) ,
45
87
} ;
46
88
47
89
const mockFetch = global . fetch as jest . MockedFunction < typeof fetch > ;
48
- mockFetch . mockResolvedValue ( mockResponse as Response ) ;
90
+ mockFetch
91
+ . mockResolvedValueOnce ( mockFetchMetadataResponse as Response )
92
+ . mockResolvedValueOnce ( mockFetchBlobResponse as Response ) ;
49
93
50
- const result = await fetchGitHubFile < GitHubFileResponse > ( mockInstance , "test.json" , "token" ) ;
94
+ const result = await fetchGitHubFile ( mockInstance , filePath , mockToken ) ;
51
95
52
- expect ( result ) . toEqual ( mockFileContent ) ;
53
- expect ( mockFetch ) . toHaveBeenCalledWith ( "https://api.github.com/repos/owner/repo/contents/test.json?ref=main" , {
54
- headers : { Authorization : "Token token" } ,
96
+ expect ( result ) . toEqual ( fileContent ) ;
97
+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
98
+ expect ( mockFetch ) . toHaveBeenNthCalledWith ( 1 , contentsUrl , {
99
+ headers : { Authorization : `Token ${ mockToken } ` } ,
100
+ } ) ;
101
+ expect ( mockFetch ) . toHaveBeenNthCalledWith ( 2 , blobUrl , {
102
+ headers : { Authorization : `Token ${ mockToken } ` } ,
55
103
} ) ;
56
104
} ) ;
57
105
58
- it ( "should throw GitHubFileNotFoundError when file is not found" , async ( ) => {
106
+ it ( "should throw GitHubFileNotFoundError when file is not found (404 on metadata fetch) " , async ( ) => {
59
107
const mockResponse : Partial < Response > = {
60
108
ok : false ,
61
109
status : 404 ,
62
110
statusText : "Not Found" ,
63
111
} ;
64
112
const mockFetch = global . fetch as jest . MockedFunction < typeof fetch > ;
65
- mockFetch . mockResolvedValue ( mockResponse as Response ) ;
66
- await expect ( fetchGitHubFile < GitHubFileResponse > ( mockInstance , "nonexistent.json" , "token" ) ) . rejects . toThrow (
67
- GitHubFileNotFoundError ,
113
+ mockFetch . mockResolvedValueOnce ( mockResponse as Response ) ;
114
+
115
+ await expect ( fetchGitHubFile ( mockInstance , "nonexistent.json" , mockToken ) ) . rejects . toThrow ( GitHubFileNotFoundError ) ;
116
+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 1 ) ;
117
+ expect ( mockFetch ) . toHaveBeenCalledWith (
118
+ "https://api.github.com/repos/owner/repo/contents/nonexistent.json?ref=main" ,
119
+ expect . any ( Object ) ,
68
120
) ;
69
121
} ) ;
70
122
71
- it ( "should throw GitHubNetworkError on network errors" , async ( ) => {
123
+ it ( "should throw GitHubNetworkError on network errors during metadata fetch " , async ( ) => {
72
124
const mockFetch = global . fetch as jest . MockedFunction < typeof fetch > ;
73
- mockFetch . mockRejectedValue ( new TypeError ( "Failed to fetch" ) ) ;
74
- await expect ( fetchGitHubFile < GitHubFileResponse > ( mockInstance , "test.json" , "token" ) ) . rejects . toThrow (
75
- GitHubNetworkError ,
76
- ) ;
125
+ mockFetch . mockRejectedValueOnce ( new TypeError ( "Failed to fetch" ) ) ;
126
+
127
+ await expect ( fetchGitHubFile ( mockInstance , filePath , mockToken ) ) . rejects . toThrow ( GitHubNetworkError ) ;
128
+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 1 ) ;
129
+ expect ( mockFetch ) . toHaveBeenCalledWith ( contentsUrl , expect . any ( Object ) ) ;
77
130
} ) ;
78
131
79
- it ( "should throw GitHubAccessError on invalid JSON response" , async ( ) => {
132
+ it ( "should throw GitHubAccessError on invalid JSON response during metadata fetch " , async ( ) => {
80
133
const mockResponse : Partial < Response > = {
81
134
ok : true ,
82
135
status : 200 ,
83
136
statusText : "OK" ,
84
- json : jest . fn < ( ) => Promise < unknown > > ( ) . mockRejectedValue ( new Error ( "Invalid JSON" ) ) ,
137
+ json : jest . fn < ( ) => Promise < unknown > > ( ) . mockRejectedValue ( new SyntaxError ( "Invalid JSON" ) ) ,
85
138
} ;
86
139
const mockFetch = global . fetch as jest . MockedFunction < typeof fetch > ;
87
- mockFetch . mockResolvedValue ( mockResponse as Response ) ;
88
- await expect ( fetchGitHubFile < GitHubFileResponse > ( mockInstance , "test.json" , "token" ) ) . rejects . toThrow (
89
- GitHubAccessError ,
90
- ) ;
140
+ mockFetch . mockResolvedValueOnce ( mockResponse as Response ) ;
141
+
142
+ await expect ( fetchGitHubFile ( mockInstance , filePath , mockToken ) ) . rejects . toThrow ( GitHubAccessError ) ;
143
+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 1 ) ;
144
+ expect ( mockFetch ) . toHaveBeenCalledWith ( contentsUrl , expect . any ( Object ) ) ;
91
145
} ) ;
92
146
93
- it ( "should throw GitHubAccessError on empty response" , async ( ) => {
147
+ it ( "should throw GitHubAccessError on empty response during metadata fetch " , async ( ) => {
94
148
const mockResponse : Partial < Response > = {
95
149
ok : true ,
96
150
status : 200 ,
97
151
statusText : "OK" ,
98
152
json : jest . fn < ( ) => Promise < unknown > > ( ) . mockResolvedValue ( null ) ,
99
153
} ;
100
154
const mockFetch = global . fetch as jest . MockedFunction < typeof fetch > ;
101
- mockFetch . mockResolvedValue ( mockResponse as Response ) ;
102
- await expect ( fetchGitHubFile < GitHubFileResponse > ( mockInstance , "test.json" , "token" ) ) . rejects . toThrow (
103
- GitHubAccessError ,
104
- ) ;
155
+ mockFetch . mockResolvedValueOnce ( mockResponse as Response ) ;
156
+
157
+ await expect ( fetchGitHubFile ( mockInstance , filePath , mockToken ) ) . rejects . toThrow ( GitHubAccessError ) ;
158
+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 1 ) ;
159
+ expect ( mockFetch ) . toHaveBeenCalledWith ( contentsUrl , expect . any ( Object ) ) ;
105
160
} ) ;
106
161
107
- it ( "should throw GitHubAccessError on unauthorized access" , async ( ) => {
162
+ it ( "should throw GitHubAccessError on unauthorized access during metadata fetch " , async ( ) => {
108
163
const mockResponse : Partial < Response > = {
109
164
ok : false ,
110
165
status : 401 ,
111
166
statusText : "Unauthorized" ,
112
167
} ;
113
168
const mockFetch = global . fetch as jest . MockedFunction < typeof fetch > ;
114
- mockFetch . mockResolvedValue ( mockResponse as Response ) ;
115
- await expect ( fetchGitHubFile < GitHubFileResponse > ( mockInstance , "test.json" , "token" ) ) . rejects . toThrow (
116
- GitHubAccessError ,
117
- ) ;
169
+ mockFetch . mockResolvedValueOnce ( mockResponse as Response ) ;
170
+
171
+ await expect ( fetchGitHubFile ( mockInstance , filePath , mockToken ) ) . rejects . toThrow ( GitHubAccessError ) ;
172
+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 1 ) ;
173
+ expect ( mockFetch ) . toHaveBeenCalledWith ( contentsUrl , expect . any ( Object ) ) ;
118
174
} ) ;
119
175
120
- it ( "should include proper error details in GitHubAccessError" , async ( ) => {
176
+ it ( "should include proper error details in GitHubAccessError from metadata fetch " , async ( ) => {
121
177
const mockResponse : Partial < Response > = {
122
178
ok : false ,
123
179
status : 401 ,
124
180
statusText : "Unauthorized" ,
125
181
} ;
126
182
const mockFetch = global . fetch as jest . MockedFunction < typeof fetch > ;
127
- mockFetch . mockResolvedValue ( mockResponse as Response ) ;
183
+ mockFetch . mockResolvedValueOnce ( mockResponse as Response ) ;
184
+
128
185
try {
129
- await fetchGitHubFile < GitHubFileResponse > ( mockInstance , "test.json" , "token" ) ;
186
+ await fetchGitHubFile ( mockInstance , filePath , mockToken ) ;
187
+
188
+ expect ( true ) . toBe ( false ) ;
130
189
} catch ( error ) {
131
190
expect ( error ) . toBeInstanceOf ( GitHubAccessError ) ;
132
191
if ( error instanceof GitHubAccessError ) {
133
- expect ( error . errorItem . target ) . toBe ( "test.json" ) ;
192
+ expect ( error . errorItem . target ) . toBe ( filePath ) ;
134
193
expect ( error . errorItem . details ) . toBeDefined ( ) ;
135
194
expect ( error . errorItem . details ! [ 0 ] . code ) . toBe ( `HTTP_401` ) ;
136
195
}
0 commit comments