Skip to content

Commit 4546ed6

Browse files
committed
integration tests: commands
Add integration tests for the following commands, which represent core bundle server functionality: 1. init 2. delete 3. update 4. start 5. stop Signed-off-by: Lessley Dennington <[email protected]>
1 parent c5d62c2 commit 4546ed6

File tree

10 files changed

+262
-6
lines changed

10 files changed

+262
-6
lines changed

test/e2e/features/step_definitions/bundleServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Given('the bundle server has been initialized with the remote repo', async funct
66
if (this.remote === undefined) {
77
throw new Error("Remote repository is not initialized")
88
}
9-
utils.assertStatus(0, this.bundleServer.init(this.remote))
9+
utils.assertStatus(0, this.bundleServer.init(this.remote, 'e2e'))
1010
})
1111

1212
Given('the bundle server was updated for the remote repo', async function (this: EndToEndBundleServerWorld) {

test/integration/cucumber.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const common = {
2+
requireModule: ['ts-node/register'],
3+
require: [`${__dirname}/features/**/*.ts`, `${__dirname}/../shared/features/**/*.ts`],
4+
publishQuiet: true,
5+
format: ['progress'],
6+
formatOptions: {
7+
snippetInterface: 'async-await'
8+
},
9+
worldParameters: {
10+
bundleServerCommand: `${__dirname}/../../bin/git-bundle-server`,
11+
bundleWebServerCommand: `${__dirname}/../../bin/git-bundle-web-server`,
12+
trashDirectoryBase: `${__dirname}/../../_test/integration`
13+
}
14+
}
15+
16+
module.exports = {
17+
default: {
18+
...common,
19+
},
20+
offline: {
21+
...common,
22+
tags: 'not @online',
23+
},
24+
ci: {
25+
...common,
26+
tags: 'not @daemon',
27+
},
28+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
Feature: Bundle server command tests
2+
3+
Background: The bundle web server is running
4+
Given the bundle web server was started at port 8080
5+
6+
@online
7+
Scenario: The init command initializes a bundle server repository
8+
Given no bundle server repository exists at route 'integration/asset-hash'
9+
When I run the bundle server CLI command 'init https://github.com/vdye/asset-hash.git integration/asset-hash'
10+
Then a bundle server repository exists at route 'integration/asset-hash'
11+
12+
@online
13+
Scenario: The delete command removes route configuration and repository data
14+
Given a remote repository 'https://github.com/vdye/asset-hash.git'
15+
Given a bundle server repository is created at route 'integration/asset-hash' for the remote
16+
When I run the bundle server CLI command 'delete integration/asset-hash'
17+
Then the route configuration and repository data at 'integration/asset-hash' are removed
18+
19+
Scenario: The update command fetches the latest remote content and updates the bundle list
20+
Given no bundle server repository exists at route 'integration/bundle'
21+
Given a new remote repository with main branch 'main'
22+
Given the remote is cloned
23+
Given 5 commits are pushed to the remote branch 'main'
24+
Given a bundle server repository is created at route 'integration/bundle' for the remote
25+
Given 2 commits are pushed to the remote branch 'main'
26+
When I run the bundle server CLI command 'update integration/bundle'
27+
Then the bundles are fetched and the bundle list is updated
28+
29+
Scenario: The stop command updates the routes file
30+
Given no bundle server repository exists at route 'integration/stop'
31+
Given a new remote repository with main branch 'main'
32+
Given a bundle server repository is created at route 'integration/stop' for the remote
33+
When I run the bundle server CLI command 'stop integration/stop'
34+
Then the route is removed from the routes file
35+
36+
Scenario: The start command updates the routes file
37+
Given no bundle server repository exists at route 'integration/start'
38+
Given a new remote repository with main branch 'main'
39+
Given a bundle server repository is created at route 'integration/start' for the remote
40+
When I run the bundle server CLI command 'stop integration/start'
41+
When I run the bundle server CLI command 'start integration/start'
42+
Then the route exists in the routes file
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as assert from 'assert'
2+
import { IntegrationBundleServerWorld } from '../support/world'
3+
import { Given, Then } from '@cucumber/cucumber'
4+
import * as utils from '../../../shared/support/utils'
5+
import * as fs from 'fs'
6+
7+
Given('a bundle server repository is created at route {string} for the remote', async function (this: IntegrationBundleServerWorld, route: string) {
8+
if (!this.remote) {
9+
throw new Error("Remote has not been initialized")
10+
}
11+
this.bundleServer.init(this.remote, 'integration', route)
12+
})
13+
14+
Given('no bundle server repository exists at route {string}', async function (this: IntegrationBundleServerWorld, route: string) {
15+
var repoPath = utils.repoRoot(route)
16+
if (fs.existsSync(repoPath)) {
17+
throw new Error(`Repo already exists at ${repoPath}`)
18+
}
19+
})
20+
21+
Then('a bundle server repository exists at route {string}', async function (this: IntegrationBundleServerWorld, route: string) {
22+
var repoRoot = utils.repoRoot(route)
23+
assert.equal(fs.existsSync(repoRoot), true)
24+
assert.equal(fs.existsSync(`${repoRoot}/.git`), false)
25+
assert.equal(fs.existsSync(`${repoRoot}/HEAD`), true)
26+
assert.equal(fs.existsSync(`${repoRoot}/bundle-list.json`), true)
27+
28+
// Set route for cleanup
29+
this.bundleServer.route = route
30+
})
31+
32+
Then('the route configuration and repository data at {string} are removed', async function (this: IntegrationBundleServerWorld, route: string) {
33+
var repoRoot = utils.repoRoot(route)
34+
var routeData = fs.readFileSync(utils.routesPath())
35+
36+
assert.equal(fs.existsSync(repoRoot), false)
37+
assert.equal(routeData.includes(route), false)
38+
39+
// Reset route to be ignored in cleanup
40+
this.bundleServer.route = undefined
41+
})
42+
43+
Then('the bundles are fetched and the bundle list is updated', async function (this: IntegrationBundleServerWorld) {
44+
assert.strictEqual(this.commandResult?.stdout.toString()
45+
.includes('Updating bundle list\n' +
46+
'Writing updated bundle list\n' +
47+
'Update complete'), true)
48+
49+
if (this.bundleServer.initialBundleCount) {
50+
const currentBundleCount = this.bundleServer.getBundleCount()
51+
assert.strictEqual(currentBundleCount > this.bundleServer.initialBundleCount, true)
52+
} else {
53+
throw new Error("Bundle server not initialized")
54+
}
55+
})
56+
57+
Then('the route is removed from the routes file', async function (this: IntegrationBundleServerWorld) {
58+
if (this.bundleServer.route) {
59+
var routesPath = utils.routesPath()
60+
var data = fs.readFileSync(routesPath);
61+
assert.strictEqual(data.includes(this.bundleServer.route), false)
62+
}
63+
})
64+
65+
Then('the route exists in the routes file', async function (this: IntegrationBundleServerWorld) {
66+
if (this.bundleServer.route) {
67+
var routesPath = utils.routesPath()
68+
var data = fs.readFileSync(routesPath);
69+
assert.strictEqual(data.includes(this.bundleServer.route), true)
70+
} else {
71+
throw new Error("Route not set")
72+
}
73+
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { When } from "@cucumber/cucumber"
2+
import { IntegrationBundleServerWorld } from '../support/world'
3+
4+
When('I run the bundle server CLI command {string}', async function (this: IntegrationBundleServerWorld, command: string) {
5+
this.runCommand(command)
6+
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Given } from '@cucumber/cucumber'
2+
import { IntegrationBundleServerWorld } from '../support/world'
3+
import * as utils from '../../../shared/support/utils'
4+
import { randomBytes } from 'crypto'
5+
6+
Given('the remote is cloned', async function (this: IntegrationBundleServerWorld) {
7+
this.cloneRepository()
8+
})
9+
10+
Given('{int} commits are pushed to the remote branch {string}', async function (this: IntegrationBundleServerWorld, commitNum: number, branch: string) {
11+
if (this.local) {
12+
for (let i = 0; i < commitNum; i++) {
13+
utils.assertStatus(0, this.runShell(`echo ${randomBytes(16).toString('hex')} >${this.local.root}/README.md`))
14+
utils.assertStatus(0, utils.runGit("-C", this.local.root, "add", "README.md"))
15+
utils.assertStatus(0, utils.runGit("-C", this.local.root, "commit", "-m", `test ${i + 1}`))
16+
}
17+
} else {
18+
throw new Error("Local repo not initialized")
19+
}
20+
21+
if (this.remote) {
22+
utils.assertStatus(0, utils.runGit("-C", this.local.root, "push", "origin", branch))
23+
} else {
24+
throw new Error("Remote repo not initialized")
25+
}
26+
})
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as child_process from 'child_process'
2+
import { RemoteRepo } from '../../../shared/classes/remote'
3+
import { ClonedRepository } from '../../../shared/classes/repository'
4+
import { BundleServerWorldBase } from '../../../shared/support/world'
5+
import { setWorldConstructor } from '@cucumber/cucumber'
6+
7+
export class IntegrationBundleServerWorld extends BundleServerWorldBase {
8+
remote: RemoteRepo | undefined
9+
local: ClonedRepository | undefined
10+
11+
commandResult: child_process.SpawnSyncReturns<Buffer> | undefined
12+
13+
runCommand(commandArgs: string): void {
14+
this.commandResult = child_process.spawnSync(`${this.parameters.bundleServerCommand} ${commandArgs}`, [], { shell: true })
15+
}
16+
17+
runShell(command: string, ...args: string[]): child_process.SpawnSyncReturns<Buffer> {
18+
return child_process.spawnSync(command, args, { shell: true })
19+
}
20+
21+
cloneRepository(): void {
22+
if (!this.remote) {
23+
throw new Error("Remote repository is not initialized")
24+
}
25+
26+
const repoRoot = `${this.trashDirectory}/client`
27+
this.local = new ClonedRepository(this.remote, repoRoot)
28+
}
29+
}
30+
31+
setWorldConstructor(IntegrationBundleServerWorld)

test/shared/classes/bundleServer.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { randomBytes } from 'crypto'
22
import * as child_process from 'child_process'
33
import { RemoteRepo } from './remote'
4+
import * as fs from 'fs'
5+
import * as utils from '../support/utils'
46

57
export class BundleServer {
68
private bundleServerCmd: string
@@ -11,7 +13,8 @@ export class BundleServer {
1113
private bundleUriBase: string | undefined
1214

1315
// Remote repo info (for now, only support one per test)
14-
private route: string | undefined
16+
route: string | undefined
17+
initialBundleCount: number | undefined
1518

1619
constructor(bundleServerCmd: string, bundleWebServerCmd: string) {
1720
this.bundleServerCmd = bundleServerCmd
@@ -26,9 +29,21 @@ export class BundleServer {
2629
this.bundleUriBase = `http://localhost:${port}/`
2730
}
2831

29-
init(remote: RemoteRepo): child_process.SpawnSyncReturns<Buffer> {
30-
this.route = `e2e/${randomBytes(8).toString('hex')}`
31-
return child_process.spawnSync(this.bundleServerCmd, ["init", remote.remoteUri, this.route])
32+
init(remote: RemoteRepo, routePrefix: string, route: string = ""): child_process.SpawnSyncReturns<Buffer> {
33+
if (route === "") {
34+
route = `${routePrefix}/${randomBytes(8).toString('hex')}`
35+
}
36+
this.route = route
37+
38+
const repoPath = utils.repoRoot(route)
39+
if (fs.existsSync(repoPath)) {
40+
throw new Error("Bundle server repository already exists")
41+
}
42+
43+
const result = child_process.spawnSync(this.bundleServerCmd, ["init", remote.remoteUri, this.route])
44+
this.initialBundleCount = this.getBundleCount()
45+
46+
return result
3247
}
3348

3449
update(): child_process.SpawnSyncReturns<Buffer> {
@@ -49,6 +64,23 @@ export class BundleServer {
4964
return this.bundleUriBase + this.route
5065
}
5166

67+
getBundleCount(): number {
68+
if (!this.route) {
69+
throw new Error("Route is not defined")
70+
}
71+
72+
var matches: string[] = [];
73+
const files = fs.readdirSync(`${utils.wwwPath()}/${this.route}`);
74+
75+
for (const file of files) {
76+
if (file.endsWith('.bundle')) {
77+
matches.push(file);
78+
}
79+
}
80+
81+
return matches.length;
82+
}
83+
5284
cleanup(): void {
5385
if (this.webServerProcess) {
5486
const killed = this.webServerProcess.kill('SIGINT')

test/shared/classes/repository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import * as utils from '../support/utils'
44

55
export class ClonedRepository {
66
private initialized: boolean
7-
private root: string
87
private remote: RemoteRepo | undefined
98

9+
root: string
1010
cloneResult: child_process.SpawnSyncReturns<Buffer>
1111
cloneTimeMs: number
1212

test/shared/support/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import * as assert from 'assert'
22
import * as child_process from 'child_process'
33
import * as path from 'path'
44

5+
const bundleRoot = `${process.env.HOME}/git-bundle-server`
6+
57
export function absPath(pathParam: string): string {
68
// Convert a given path (either relative to the top-level project directory or
79
// absolute) to an absolute path
@@ -24,3 +26,19 @@ export function assertStatus(expectedStatusCode: number, result: child_process.S
2426
assert.strictEqual(result.status, expectedStatusCode,
2527
`${message ?? "Invalid status code"}:\n\tstdout: ${result.stdout.toString()}\n\tstderr: ${result.stderr.toString()}`)
2628
}
29+
30+
export function wwwPath(): string {
31+
return path.resolve(bundleRoot, "www")
32+
}
33+
34+
export function repoRoot(pathParam: string): string {
35+
if (!path.isAbsolute(pathParam)) {
36+
return path.resolve(bundleRoot, "git", pathParam)
37+
} else {
38+
return pathParam
39+
}
40+
}
41+
42+
export function routesPath(): string {
43+
return path.resolve(bundleRoot, "routes")
44+
}

0 commit comments

Comments
 (0)