Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions E2E_TEST_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,31 +491,31 @@ Tier: destructive for all three files. Require ephemeral mode.

### SDK prerequisite

- [ ] Correct `defaultRegistryId` versus backend `defaultPushRegistryId` in `caprover-api`.
- [ ] Publish and consume the corrected package.
- [x] Correct `defaultRegistryId` versus backend `defaultPushRegistryId` in `caprover-api` ([SDK PR #17](https://github.com/caprover/caprover-api/pull/17)).
- [x] Publish and consume the corrected `caprover-api@0.0.25` package.

### Lightweight registry contracts

- [ ] Read the initial registry list.
- [ ] Reject an unknown default registry ID.
- [ ] Specify a reachable registry endpoint and deliberately invalid credentials; verify the expected authentication rejection and registry error status.
- [ ] Treat DNS failures, connection failures, and timeouts as test failures. They must not satisfy the invalid-credentials assertion.
- [x] Read the initial registry list.
- [x] Reject an unknown default registry ID.
- [x] Specify a reachable registry endpoint and deliberately invalid credentials; verify the expected authentication rejection and registry error status.
- [x] Treat DNS failures, connection failures, and timeouts as test failures. They must not satisfy the invalid-credentials assertion.

### GoAccess

- [ ] Preserve settings.
- [ ] Enable GoAccess and generate traffic.
- [ ] Retrieve report listings and a live report.
- [ ] Verify missing-app and missing-report behavior.
- [ ] Restore settings.
- [x] Preserve settings.
- [x] Enable GoAccess and generate traffic.
- [x] Retrieve report listings and a live report.
- [x] Verify missing-app and missing-report behavior.
- [x] Restore settings.

### NetData

- [ ] Preserve settings.
- [ ] Enable NetData with notifications disabled.
- [ ] Verify service and proxied endpoint.
- [ ] Disable NetData and verify removal.
- [ ] Restore settings.
- [x] Preserve settings.
- [x] Enable NetData with notifications disabled.
- [x] Verify container and proxied endpoint.
- [x] Disable NetData and verify removal.
- [x] Restore settings.

Full self-hosted registry build-and-push coverage belongs in the controlled SSL workflow because enabling it requests a real certificate.

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"test:all": "vitest run --mode all"
},
"dependencies": {
"caprover-api": "0.0.23",
"caprover-api": "0.0.25",
"ssh2": "1.17.0"
},
"devDependencies": {
Expand Down
77 changes: 77 additions & 0 deletions src/clients/caprover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ type OneClickAppRepositories = Awaited<
type OneClickDeploymentState = Awaited<
ReturnType<CapRoverAPI['getOneClickAppDeployProgress']>
>
type RegistriesResponse = Awaited<
ReturnType<CapRoverAPI['getDockerRegistries']>
>
type GoAccessInfo = Awaited<ReturnType<CapRoverAPI['getGoAccessInfo']>>
type GoAccessSettings = Parameters<CapRoverAPI['updateGoAccessInfo']>[0]
type GoAccessReport = Awaited<ReturnType<CapRoverAPI['getGoAccessReports']>>
type NetDataInfo = Awaited<ReturnType<CapRoverAPI['getNetDataInfo']>>

export interface OneClickValuePair {
key: string
Expand Down Expand Up @@ -104,6 +111,72 @@ export class CapRoverClient {
return this.request(() => this.api.getAllNodes(), 'listing Swarm nodes')
}

getDockerRegistries(): Promise<RegistriesResponse> {
return this.request(
() => this.api.getDockerRegistries(),
'listing Docker registries'
)
}

addDockerRegistry(registry: CapRoverModels.IRegistryInfo): Promise<void> {
return this.request(
() => this.api.addDockerRegistry(registry),
'adding Docker registry'
)
}

setDefaultPushDockerRegistry(registryId: string): Promise<void> {
return this.request(
() => this.api.setDefaultPushDockerRegistry(registryId),
'selecting default push registry'
)
}

getGoAccessInfo(): Promise<GoAccessInfo> {
return this.request(
() => this.api.getGoAccessInfo(),
'retrieving GoAccess settings'
)
}

updateGoAccessInfo(settings: GoAccessSettings): Promise<void> {
return this.request(
() => this.api.updateGoAccessInfo(settings),
'updating GoAccess settings'
)
}

getGoAccessReports(appName: string): Promise<GoAccessReport> {
return this.request(
() => this.api.getGoAccessReports(appName),
`listing GoAccess reports for ${appName}`,
DEPLOYMENT_TIMEOUT_MS
)
}

getGoAccessReport(reportUrl: string): Promise<string> {
return this.request(
() => this.api.getGoAccessReport(reportUrl),
'retrieving GoAccess report',
DEPLOYMENT_TIMEOUT_MS
)
}

getNetDataInfo(): Promise<NetDataInfo> {
return this.request(
() => this.api.getNetDataInfo(),
'retrieving NetData settings'
)
}

updateNetDataInfo(settings: NetDataInfo): Promise<void> {
return this.request(
() => this.api.updateNetDataInfo(settings),
'updating NetData settings',
DEPLOYMENT_TIMEOUT_MS
)
}

getProFeaturesState(): Promise<ProFeaturesState> {
return this.request(
() => this.api.getProFeaturesState(),
Expand Down Expand Up @@ -489,4 +562,8 @@ export type {
OneClickAppRepositories,
OneClickAppsResponse,
OneClickDeploymentState,
RegistriesResponse,
GoAccessInfo,
GoAccessSettings,
NetDataInfo,
}
27 changes: 27 additions & 0 deletions src/inspectors/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,33 @@ const VOLUME_MARKER_PATH = '/e2e-volume/marker'
export class DockerInspector {
constructor(private readonly ssh: SshClient) {}

async getContainerState(
name: string
): Promise<'running' | 'stopped' | 'absent'> {
if (!/^captain-(?:goaccess|netdata)-container$/.test(name)) {
throw new Error(`Unexpected standalone container: ${name}`)
}
const result = await this.ssh.exec(`docker inspect ${shellQuote(name)}`)
if (result.exitCode !== 0) {
if (/no such (?:object|container)/i.test(result.stderr))
return 'absent'
throw new Error(
`docker inspect failed for ${name}: ${result.stderr.trim()}`
)
}
const containers = parseJson<Array<{ State?: { Running?: boolean } }>>(
result.stdout,
`container ${name}`
)
if (
containers.length !== 1 ||
typeof containers[0]?.State?.Running !== 'boolean'
) {
throw new Error(`Docker returned an invalid state for ${name}`)
}
return containers[0].State.Running ? 'running' : 'stopped'
}

async validateEnvironment(): Promise<void> {
const result = await this.exec("docker info --format '{{json .Swarm}}'")
const swarm = parseJson<DockerSwarmInfo>(result.stdout, 'Docker info')
Expand Down
87 changes: 87 additions & 0 deletions tests/goaccess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { expect, test } from 'vitest'
import { GoAccessSettings } from '../src/clients/caprover'
import { waitForImage } from '../src/helpers/deployment'
import { createTestNames } from '../src/helpers/names'
import { eventually } from '../src/helpers/retry'
import { cleanUpApp, withTestContext } from '../src/helpers/test-context'
import { requireEphemeral } from '../src/test-selection'

const IMAGE =
'nginx:1.29.8-alpine@sha256:5616878291a2eed594aee8db4dade5878cf7edcb475e59193904b198d9b830de'
const CONTAINER = 'captain-goaccess-container'

test('GoAccess generates a live report and restores its original configuration', async () => {
requireEphemeral()
await withTestContext(async (context, cleanup, rootDomain) => {
const api = context.caprover
const { runId } = createTestNames()
const appName = `e2e-${runId}-goaccess`
const appUrl = `http://${appName}.${rootDomain}`

cleanUpApp(context, cleanup, appName)
await api.createApp(appName)
await api.deployImage(appName, IMAGE)
await waitForImage(context, appName, IMAGE)
await context.http.waitUntilReachable(appUrl, 'Welcome to nginx')

const original = await api.getGoAccessInfo()
cleanup.add(async () => {
await api.updateGoAccessInfo(original as GoAccessSettings)
await eventually(
async () => {
expect(await api.getGoAccessInfo()).toEqual(original)
expect(
await context.docker.getContainerState(CONTAINER)
).toBe(original.isEnabled ? 'running' : 'absent')
},
{ description: 'GoAccess settings and container restoration' }
)
})

const enabled: GoAccessSettings = {
isEnabled: true,
data: { rotationFrequencyCron: '0 0 1 * *', logRetentionDays: 7 },
}
await api.updateGoAccessInfo(enabled)
await eventually(
async () => {
expect(await api.getGoAccessInfo()).toEqual(enabled)
expect(await context.docker.getContainerState(CONTAINER)).toBe(
'running'
)
},
{ description: 'GoAccess container to start' }
)

for (let index = 0; index < 3; index++) {
expect((await context.http.get(appUrl)).status).toBe(200)
}
const reports = await api.getGoAccessReports(appName)
const live = reports.find(
(report) =>
report.domainName === `${appName}.${rootDomain}` &&
report.name.endsWith('--Live.html')
)
expect(live?.url).toMatch(/^\/user\/system\/goaccess\//)
await eventually(
async () => {
const html = await api.getGoAccessReport(live!.url)
expect(html.length).toBeGreaterThan(100)
expect(html.toLowerCase()).toContain('<html')
},
{ timeoutMs: 60_000, description: 'GoAccess live HTML report' }
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

await expect(
api.getGoAccessReports(`e2e-missing-${runId}`)
).rejects.toMatchObject({ captainStatus: 1000 })
const missingUrl = live!.url.replace(
/--Live\.html$/,
`--missing-${runId}.html`
)
expect(missingUrl).not.toBe(live!.url)
await expect(api.getGoAccessReport(missingUrl)).rejects.toMatchObject({
captainStatus: 1111,
})
})
})
Loading
Loading