Skip to content

Commit b8862bc

Browse files
add integration tests for clone api (#24)
1 parent ee36065 commit b8862bc

File tree

9 files changed

+192
-8
lines changed

9 files changed

+192
-8
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Git-Essentials
2+
A collection of essential Git commands for your browser and Node.js.
3+
4+
# Usage Examples
5+
6+
## Node.js
7+
```typescript
8+
import fs from 'fs/promises'
9+
import { clone, HttpClients } from 'git-essentials'
10+
11+
const http = HttpClients.makeNodeHttpClient()
12+
const dir = 'repos/Welcome'
13+
const url = 'https://github.com/NotesHubApp/Welcome.git'
14+
15+
await clone({ fs, http, dir, url })
16+
```
17+
18+
## Browser
19+
```typescript
20+
import { clone, FsClients, HttpClients } from 'git-essentials'
21+
22+
const corsProxyUrlTransformer = (originalUrl: string) => {
23+
return `https://www.noteshub.app/api/cors-proxy.ts?url=${encodeURIComponent(originalUrl)}`
24+
}
25+
26+
const fs = new FsClients.InMemoryFsClient()
27+
const http = HttpClients.makeWebHttpClient({ transformRequestUrl: corsProxyUrlTransformer })
28+
const dir = 'repos/Welcome'
29+
const url = 'https://github.com/NotesHubApp/Welcome.git'
30+
31+
await clone({ fs, http, dir, url })
32+
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "git-essentials",
3-
"version": "0.3.29",
3+
"version": "0.3.30",
44
"description": "The essential GIT commands",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

scripts/gen-http-fixture.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ function printFixtureForLocalRepo() {
145145
* Generate random id
146146
* @param {number} length
147147
*/
148-
function generateId(length) {
148+
function generateId(length) {
149149
let result = ''
150150
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
151151
const charactersLength = characters.length

src/clients/fs/InMemoryFsClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export class InMemoryFsClient implements FsClient {
224224
folder.children.push(makeEmptyFolder(entryName))
225225
}
226226

227-
public async rmdir(filepath: string, opts?: RMDirOptions | undefined): Promise<void> {
227+
public async rmdir(filepath: string, opts?: RMDirOptions): Promise<void> {
228228
const { folder, entry, entryName } = this.parsePath(filepath)
229229

230230
if (!entry) {
@@ -235,7 +235,7 @@ export class InMemoryFsClient implements FsClient {
235235
throw new ENOTDIR(filepath)
236236
}
237237

238-
if (entry.children.length > 0 && !(opts && opts.force)) {
238+
if (entry.children.length > 0) {
239239
throw new ENOTEMPTY(filepath)
240240
}
241241

src/clients/http/NodeHttpClient.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import https, { RequestOptions } from 'https'
2+
import { GitHttpRequest, GitHttpResponse, HttpHeaders } from '../../models'
3+
4+
5+
function request(req: GitHttpRequest): Promise<GitHttpResponse> {
6+
return new Promise<GitHttpResponse>((resolve, reject) => {
7+
const parsedRequestUrl = new URL(req.url)
8+
const requestOptions: RequestOptions = {
9+
host: parsedRequestUrl.host,
10+
path: parsedRequestUrl.pathname + parsedRequestUrl.search,
11+
method: req.method,
12+
headers: req.headers
13+
}
14+
15+
const nodeRequest = https.request(requestOptions, async (res) => {
16+
let chunks: Buffer[] = []
17+
18+
res.on('data', (chunk) => {
19+
chunks.push(chunk)
20+
}).on('end', () => {
21+
const resHeaders: HttpHeaders = {}
22+
for (const headerName in res.headers) {
23+
const headerValue = res.headers[headerName]
24+
if (typeof headerValue === 'string') {
25+
resHeaders[headerName] = headerValue
26+
} else {
27+
resHeaders[headerName] = (headerValue || []).join(';')
28+
}
29+
}
30+
31+
resolve({
32+
url: res.url!,
33+
statusCode: res.statusCode!,
34+
statusMessage: res.statusMessage!,
35+
headers: resHeaders,
36+
body: chunks
37+
})
38+
}).on('error', (e) => {
39+
reject(e)
40+
})
41+
})
42+
43+
if (req.body) {
44+
nodeRequest.write(Buffer.concat(req.body as Uint8Array[]))
45+
}
46+
47+
nodeRequest.end()
48+
})
49+
}
50+
51+
export type NodeHttpClientOptions = {}
52+
53+
export function makeNodeHttpClient(options: NodeHttpClientOptions = {}) {
54+
return { request }
55+
}

src/clients/http/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './WebHttpClient'
2+
export * from './NodeHttpClient'

src/models/FsClient.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ export type WriteOpts = EncodingOpts & {
66
mode?: number
77
}
88

9-
export interface RMDirOptions {
10-
force?: boolean
11-
}
9+
export interface RMDirOptions {}
1210

1311
export type Stat = {
1412
/** A bit-field describing the file type and mode. */
@@ -49,7 +47,6 @@ export type Stat = {
4947
}
5048

5149
export type StatLike = Stat & {
52-
type: 'file' | 'dir' | 'symlink';
5350
mtimeMs: number;
5451

5552
/** Returns true if the <fs.Stats> object describes a regular file. */

tests/e2e.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import path from 'path'
2+
3+
import { clone } from '../src'
4+
import { integrationContext } from './helpers/integrationContext'
5+
6+
7+
describe('e2e', () => {
8+
describe('clone', () => {
9+
it('should clone from GitHub', async () => {
10+
await integrationContext(async ({ fs, http, dir }) => {
11+
// arrange
12+
const url = 'https://github.com/NotesHubApp/Welcome.git'
13+
14+
// act
15+
await clone({
16+
fs,
17+
http,
18+
dir,
19+
url,
20+
singleBranch: true,
21+
depth: 1
22+
})
23+
24+
// assert
25+
const fileStat = await fs.lstat(path.join(dir, 'Welcome note.md'))
26+
expect(fileStat.isFile()).toBe(true)
27+
})
28+
})
29+
})
30+
})
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { FsClient, HttpClient, FsClients, HttpClients } from '../../src'
2+
import { join } from '../../src/utils/join'
3+
4+
const isBrowser = () => typeof window !== `undefined`
5+
6+
const corsProxyUrlTransformer = (originalUrl: string) => {
7+
return `https://www.noteshub.app/api/cors-proxy.ts?url=${encodeURIComponent(originalUrl)}`
8+
}
9+
10+
type IntegrationContext = {
11+
fs: FsClient
12+
http: HttpClient
13+
dir: string
14+
}
15+
16+
export async function integrationContext(action: (context: IntegrationContext) => Promise<void>) {
17+
const { fs, http } = isBrowser() ? {
18+
fs: new FsClients.InMemoryFsClient(),
19+
http: HttpClients.makeWebHttpClient({ transformRequestUrl: corsProxyUrlTransformer })
20+
} : {
21+
fs: (await import('fs')).promises,
22+
http: HttpClients.makeNodeHttpClient()
23+
}
24+
25+
const dir = generateId(20)
26+
await fs.mkdir(dir)
27+
28+
try {
29+
await action({ fs, http, dir })
30+
} finally {
31+
await deleteRecursively(fs, dir)
32+
}
33+
}
34+
35+
function generateId(length: number) {
36+
let result = ''
37+
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
38+
const charactersLength = characters.length
39+
for (let i = 0; i < length; i++) {
40+
result += characters.charAt(Math.floor(Math.random() * charactersLength))
41+
}
42+
return result
43+
}
44+
45+
async function deleteRecursively(fs: FsClient, dirname: string) {
46+
const filesToDelete: string[] = []
47+
const directoriesToDelete: string[] = []
48+
const pathsToTraverse = [dirname]
49+
50+
while (pathsToTraverse.length > 0) {
51+
const path = pathsToTraverse.pop()!
52+
53+
if ((await fs.stat(path)).isDirectory()) {
54+
directoriesToDelete.push(path)
55+
pathsToTraverse.push(
56+
...(await fs.readdir(path))!.map(subPath => join(path, subPath))
57+
)
58+
} else {
59+
filesToDelete.push(path)
60+
}
61+
}
62+
63+
for (const path of filesToDelete) {
64+
await fs.unlink(path)
65+
}
66+
for (const path of directoriesToDelete.reverse()) {
67+
await fs.rmdir(path)
68+
}
69+
}

0 commit comments

Comments
 (0)