Skip to content

Commit 9820df4

Browse files
authored
Make extra sure the core IPv4 UX is still rock solid (#3053)
make extra sure the v4 UX is still rock solid
1 parent 756faaa commit 9820df4

File tree

3 files changed

+159
-9
lines changed

3 files changed

+159
-9
lines changed

mock-api/floating-ip.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { FloatingIp } from '@oxide/api'
1111
import { instance } from './instance'
1212
import { ipPool1, ipPool2 } from './ip-pool'
1313
import type { Json } from './json-type'
14-
import { project } from './project'
14+
import { project, projectKosman } from './project'
1515

1616
// Note that IPv4 addresses should come from ranges in ip-pool-1
1717
// Note that IPv6 addresses should come from ranges in ip-pool-2
@@ -55,4 +55,18 @@ export const floatingIp3: Json<FloatingIp> = {
5555
time_modified: new Date().toISOString(),
5656
}
5757

58-
export const floatingIps = [floatingIp, floatingIp2, floatingIp3]
58+
// A v4 floating IP in the myriad silo (v4-only default pool) for testing
59+
// that floating IP operations work in silos without v6 pools
60+
export const floatingIpKosman: Json<FloatingIp> = {
61+
id: 'd2e3f4a5-6b7c-4d9e-8f1a-2b3c4d5e6f7a',
62+
name: 'kosman-float',
63+
description: 'A v4 floating IP in the myriad silo.',
64+
instance_id: undefined,
65+
ip: '123.4.56.6',
66+
ip_pool_id: ipPool1.id,
67+
project_id: projectKosman.id,
68+
time_created: new Date().toISOString(),
69+
time_modified: new Date().toISOString(),
70+
}
71+
72+
export const floatingIps = [floatingIp, floatingIp2, floatingIp3, floatingIpKosman]

test/e2e/ip-pool-silo-config.e2e.ts

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,16 @@
1414
* - no-pools silo: no IP pools (user: Antonio Gramsci)
1515
*/
1616

17-
import { floatingIp } from '@oxide/api-mocks'
17+
import { floatingIp, floatingIpKosman } from '@oxide/api-mocks'
1818

19-
import { expect, expectRowVisible, getPageAsUser, test } from './utils'
19+
import {
20+
clickRowAction,
21+
closeToast,
22+
expect,
23+
expectRowVisible,
24+
getPageAsUser,
25+
test,
26+
} from './utils'
2027

2128
test.describe('IP pool configuration: myriad silo (v4-only default)', () => {
2229
test('instance create form shows IPv4 default pool preselected', async ({ browser }) => {
@@ -63,6 +70,135 @@ test.describe('IP pool configuration: myriad silo (v4-only default)', () => {
6370
const poolDropdown = page.getByLabel('Pool')
6471
await expect(poolDropdown).toContainText('ip-pool-1')
6572
})
73+
74+
test('can create instance with v4-only default pool', async ({ browser }) => {
75+
const page = await getPageAsUser(browser, 'Aryeh Kosman')
76+
await page.goto('/projects/kosman-project/instances-new')
77+
78+
const instanceName = 'v4-silo-instance'
79+
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
80+
81+
// Select a silo image for boot disk
82+
await page.getByRole('tab', { name: 'Silo images' }).click()
83+
await page.getByPlaceholder('Select a silo image', { exact: true }).click()
84+
await page.getByRole('option', { name: 'ubuntu-22-04' }).click()
85+
86+
// Open networking accordion and verify ephemeral IP defaults
87+
await page.getByRole('button', { name: 'Networking' }).click()
88+
const ephemeralCheckbox = page.getByRole('checkbox', {
89+
name: 'Allocate and attach an ephemeral IP address',
90+
})
91+
await expect(ephemeralCheckbox).toBeChecked()
92+
await expect(page.getByLabel('Pool')).toContainText('ip-pool-1')
93+
94+
// Create instance
95+
await page.getByRole('button', { name: 'Create instance' }).click()
96+
await closeToast(page)
97+
await expect(page).toHaveURL(
98+
`/projects/kosman-project/instances/${instanceName}/storage`
99+
)
100+
101+
// Navigate to networking tab and verify ephemeral IP
102+
await page.getByRole('tab', { name: 'Networking' }).click()
103+
const externalIpTable = page.getByRole('table', { name: 'External IPs' })
104+
await expectRowVisible(externalIpTable, {
105+
Kind: 'ephemeral',
106+
Version: 'v4',
107+
'IP pool': 'ip-pool-1',
108+
})
109+
})
110+
111+
test('can detach and reattach ephemeral IP', async ({ browser }) => {
112+
const page = await getPageAsUser(browser, 'Aryeh Kosman')
113+
await page.goto('/projects/kosman-project/instances-new')
114+
115+
// Create instance with default ephemeral IP
116+
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('v4-ephemeral-test')
117+
await page.getByRole('tab', { name: 'Silo images' }).click()
118+
await page.getByPlaceholder('Select a silo image', { exact: true }).click()
119+
await page.getByRole('option', { name: 'ubuntu-22-04' }).click()
120+
await page.getByRole('button', { name: 'Create instance' }).click()
121+
await closeToast(page)
122+
await expect(page).toHaveURL(/\/instances\/v4-ephemeral-test/)
123+
124+
await page.getByRole('tab', { name: 'Networking' }).click()
125+
const externalIpTable = page.getByRole('table', { name: 'External IPs' })
126+
const ephemeralCell = externalIpTable.getByRole('cell', { name: 'ephemeral' })
127+
const attachEphemeralIpButton = page.getByRole('button', {
128+
name: 'Attach ephemeral IP',
129+
})
130+
131+
// Verify ephemeral IP is present and attach button is disabled
132+
await expect(ephemeralCell).toBeVisible()
133+
await expect(attachEphemeralIpButton).toBeDisabled()
134+
135+
// Detach the ephemeral IP
136+
await clickRowAction(page, 'ephemeral', 'Detach')
137+
await page.getByRole('button', { name: 'Confirm' }).click()
138+
await expect(ephemeralCell).toBeHidden()
139+
await expect(attachEphemeralIpButton).toBeEnabled()
140+
141+
// Reattach — ip-pool-1 should be preselected as the only v4 default
142+
await attachEphemeralIpButton.click()
143+
const modal = page.getByRole('dialog', { name: 'Attach ephemeral IP' })
144+
await expect(modal).toBeVisible()
145+
await expect(page.getByLabel('Pool')).toContainText('ip-pool-1')
146+
147+
// Verify v6 pools are not available
148+
await page.getByLabel('Pool').click()
149+
await expect(page.getByRole('option', { name: 'ip-pool-1' })).toBeVisible()
150+
await expect(page.getByRole('option', { name: 'ip-pool-3' })).toBeVisible()
151+
await expect(page.getByRole('option', { name: 'ip-pool-2' })).toBeHidden()
152+
await expect(page.getByRole('option', { name: 'ip-pool-4' })).toBeHidden()
153+
154+
// Select ip-pool-1 (close dropdown first) and attach
155+
await page.getByRole('option', { name: 'ip-pool-1' }).click()
156+
await page.getByRole('button', { name: 'Attach', exact: true }).click()
157+
await expect(modal).toBeHidden()
158+
159+
await expectRowVisible(externalIpTable, {
160+
Kind: 'ephemeral',
161+
Version: 'v4',
162+
'IP pool': 'ip-pool-1',
163+
})
164+
await expect(attachEphemeralIpButton).toBeDisabled()
165+
})
166+
167+
test('can attach floating IP', async ({ browser }) => {
168+
const page = await getPageAsUser(browser, 'Aryeh Kosman')
169+
await page.goto('/projects/kosman-project/instances-new')
170+
171+
// Create instance
172+
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('v4-floating-test')
173+
await page.getByRole('tab', { name: 'Silo images' }).click()
174+
await page.getByPlaceholder('Select a silo image', { exact: true }).click()
175+
await page.getByRole('option', { name: 'ubuntu-22-04' }).click()
176+
await page.getByRole('button', { name: 'Create instance' }).click()
177+
await closeToast(page)
178+
await expect(page).toHaveURL(/\/instances\/v4-floating-test/)
179+
180+
await page.getByRole('tab', { name: 'Networking' }).click()
181+
const externalIpTable = page.getByRole('table', { name: 'External IPs' })
182+
183+
// Attach the pre-seeded v4 floating IP
184+
const attachFloatingIpButton = page.getByRole('button', { name: 'Attach floating IP' })
185+
await attachFloatingIpButton.click()
186+
const dialog = page.getByRole('dialog')
187+
await expect(dialog).toBeVisible()
188+
await dialog.getByLabel('Floating IP').click()
189+
await page.keyboard.press('ArrowDown')
190+
await page.keyboard.press('Enter')
191+
await dialog.getByRole('button', { name: 'Attach' }).click()
192+
await expect(dialog).toBeHidden()
193+
194+
await expectRowVisible(externalIpTable, {
195+
name: floatingIpKosman.name,
196+
Kind: 'floating',
197+
})
198+
199+
// No more floating IPs available, button should be disabled
200+
await expect(attachFloatingIpButton).toBeDisabled()
201+
})
66202
})
67203

68204
test.describe('IP pool configuration: thrax silo (v6-only default)', () => {

test/e2e/ip-pools.e2e.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ test('IP pool list', async ({ page }) => {
2323

2424
await expectRowVisible(table, {
2525
name: 'ip-pool-1',
26-
'IPs REMAINING': '17 / 24',
26+
'IPs REMAINING': '16 / 24',
2727
})
2828
await expectRowVisible(table, {
2929
name: 'ip-pool-2',
@@ -335,7 +335,7 @@ test('remove range', async ({ page }) => {
335335

336336
// utilization updates
337337
await expect(page.getByText('Allocated(IPs)')).toBeVisible()
338-
await expect(page.getByText('Allocated7')).toBeVisible()
338+
await expect(page.getByText('Allocated8')).toBeVisible()
339339
await expect(page.getByText('Capacity21')).toBeVisible()
340340

341341
// go back to the pool and verify the remaining/capacity columns changed
@@ -344,7 +344,7 @@ test('remove range', async ({ page }) => {
344344
await breadcrumbs.getByRole('link', { name: 'IP Pools' }).click()
345345
await expectRowVisible(table, {
346346
name: 'ip-pool-1',
347-
'IPs REMAINING': '14 / 21',
347+
'IPs REMAINING': '13 / 21',
348348
})
349349
})
350350

@@ -353,7 +353,7 @@ test('deleting floating IP decrements utilization', async ({ page }) => {
353353
const table = page.getByRole('table')
354354
await expectRowVisible(table, {
355355
name: 'ip-pool-1',
356-
'IPs REMAINING': '17 / 24',
356+
'IPs REMAINING': '16 / 24',
357357
})
358358

359359
// go delete a floating IP
@@ -370,7 +370,7 @@ test('deleting floating IP decrements utilization', async ({ page }) => {
370370
await page.getByRole('link', { name: 'IP Pools' }).click()
371371
await expectRowVisible(table, {
372372
name: 'ip-pool-1',
373-
'IPs REMAINING': '18 / 24',
373+
'IPs REMAINING': '17 / 24',
374374
})
375375
})
376376

0 commit comments

Comments
 (0)