test: add WebSocket e2e tests
Add comprehensive end-to-end tests for the WebSocket system: - Protocol tests: auth (valid/invalid token, timeout, double auth), subscriptions (valid/invalid event, auth required, unsubscribe), message delivery (notification on team add, doer exclusion, multi-connection) - Frontend integration tests: notification badge update, dropdown rendering, and logout cleanup via browser-level Playwright tests - Comment notification test: full flow where user B mentions user A in a task comment and user A receives real-time WebSocket notification Includes ws test dependency, shared test helper utilities, and cascade-truncation of notifications when truncating users to prevent test pollution.
This commit is contained in:
parent
09232ed880
commit
4cd79088d1
|
|
@ -116,6 +116,7 @@
|
|||
"@types/is-touch-device": "1.0.3",
|
||||
"@types/node": "24.12.0",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.0",
|
||||
"@typescript-eslint/parser": "8.58.0",
|
||||
"@vitejs/plugin-vue": "6.0.5",
|
||||
|
|
@ -154,7 +155,8 @@
|
|||
"vitest": "4.1.2",
|
||||
"vue-tsc": "3.2.6",
|
||||
"wait-on": "9.0.4",
|
||||
"workbox-cli": "7.4.0"
|
||||
"workbox-cli": "7.4.0",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
|
|
|
|||
|
|
@ -205,6 +205,9 @@ importers:
|
|||
'@types/sortablejs':
|
||||
specifier: 1.15.9
|
||||
version: 1.15.9
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 8.58.0
|
||||
version: 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3))(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)
|
||||
|
|
@ -322,6 +325,9 @@ importers:
|
|||
workbox-cli:
|
||||
specifier: 7.4.0
|
||||
version: 7.4.0
|
||||
ws:
|
||||
specifier: ^8.19.0
|
||||
version: 8.19.0
|
||||
|
||||
packages:
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import {test, expect} from '../../support/fixtures'
|
||||
import {openWs, waitForMessage, authenticateWs, subscribeWs, closeWs} from '../../support/websocket'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {ProjectViewFactory} from '../../factories/project_view'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {UserProjectFactory} from '../../factories/users_project'
|
||||
import {TEST_PASSWORD} from '../../support/constants'
|
||||
import type {APIRequestContext} from '@playwright/test'
|
||||
|
||||
async function loginRaw(apiContext: APIRequestContext, user: {username: string}): Promise<{token: string}> {
|
||||
const response = await apiContext.post('login', {
|
||||
data: {username: user.username, password: TEST_PASSWORD},
|
||||
})
|
||||
return response.json()
|
||||
}
|
||||
|
||||
test.describe('WebSocket Comment Notifications', () => {
|
||||
|
||||
test('receives notification when mentioned in a task comment', async ({apiContext, userToken, currentUser}) => {
|
||||
const ws = await openWs()
|
||||
try {
|
||||
await authenticateWs(ws, userToken)
|
||||
subscribeWs(ws, 'notification.created')
|
||||
|
||||
// Create a second user who will post the comment
|
||||
const [commenter] = await UserFactory.create(1, {id: 100}, false)
|
||||
const {token: commenterToken} = await loginRaw(apiContext, commenter)
|
||||
|
||||
// Seed a project owned by the commenter with a task
|
||||
await ProjectFactory.create(1, {id: 100, owner_id: 100}, false)
|
||||
await ProjectViewFactory.create(1, {id: 100, project_id: 100}, false)
|
||||
await TaskFactory.create(1, {id: 100, project_id: 100, created_by_id: 100}, false)
|
||||
|
||||
// Share the project with currentUser so the mention access check passes
|
||||
await UserProjectFactory.create(1, {id: 100, project_id: 100, user_id: 1}, false)
|
||||
|
||||
// Commenter posts a comment mentioning currentUser
|
||||
const commentBody = `<p>Hey <mention-user data-id="${currentUser.username}">@${currentUser.username}</mention-user> check this out</p>`
|
||||
const commentResponse = await apiContext.put('tasks/100/comments', {
|
||||
data: {comment: commentBody},
|
||||
headers: {Authorization: `Bearer ${commenterToken}`},
|
||||
})
|
||||
expect(commentResponse.ok()).toBe(true)
|
||||
|
||||
// currentUser should receive the notification via WebSocket
|
||||
const msg = await waitForMessage(ws, 15000)
|
||||
expect(msg.event).toBe('notification.created')
|
||||
expect(msg.data).toBeDefined()
|
||||
|
||||
// The notification payload must include a valid created timestamp (not zero)
|
||||
const created = new Date(msg.data.created)
|
||||
expect(created.getFullYear()).toBeGreaterThanOrEqual(2020)
|
||||
} finally {
|
||||
closeWs(ws)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import {test, expect} from '../../support/fixtures'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {TEST_PASSWORD} from '../../support/constants'
|
||||
|
||||
test.describe('WebSocket Frontend Integration', () => {
|
||||
|
||||
test('notification badge updates in real-time when added to team', async ({
|
||||
authenticatedPage: page,
|
||||
apiContext,
|
||||
currentUser,
|
||||
}) => {
|
||||
// Navigate to the app so WebSocket connects
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Verify no unread indicator initially
|
||||
await expect(page.locator('.notifications .unread-indicator')).toHaveCount(0)
|
||||
|
||||
// Create a second user who will add currentUser to a team
|
||||
const [userA] = await UserFactory.create(1, {id: 100}, false)
|
||||
const loginResponse = await apiContext.post('login', {
|
||||
data: {username: userA.username, password: TEST_PASSWORD},
|
||||
})
|
||||
const {token: tokenA} = await loginResponse.json()
|
||||
|
||||
// User A creates a team
|
||||
const teamResponse = await apiContext.put('teams', {
|
||||
data: {name: 'Real-Time Test Team'},
|
||||
headers: {Authorization: `Bearer ${tokenA}`},
|
||||
})
|
||||
const team = await teamResponse.json()
|
||||
|
||||
// User A adds currentUser to the team — this triggers a notification
|
||||
await apiContext.put(`teams/${team.id}/members`, {
|
||||
data: {username: currentUser.username},
|
||||
headers: {Authorization: `Bearer ${tokenA}`},
|
||||
})
|
||||
|
||||
// The unread indicator should appear without page refresh
|
||||
await expect(page.locator('.notifications .unread-indicator')).toBeVisible({
|
||||
timeout: 10000,
|
||||
})
|
||||
})
|
||||
|
||||
test('notification appears in dropdown after real-time delivery', async ({
|
||||
authenticatedPage: page,
|
||||
apiContext,
|
||||
currentUser,
|
||||
}) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Create user A and trigger notification
|
||||
const [userA] = await UserFactory.create(1, {id: 100}, false)
|
||||
const loginResponse = await apiContext.post('login', {
|
||||
data: {username: userA.username, password: TEST_PASSWORD},
|
||||
})
|
||||
const {token: tokenA} = await loginResponse.json()
|
||||
|
||||
const teamResponse = await apiContext.put('teams', {
|
||||
data: {name: 'Dropdown Test Team'},
|
||||
headers: {Authorization: `Bearer ${tokenA}`},
|
||||
})
|
||||
const team = await teamResponse.json()
|
||||
|
||||
await apiContext.put(`teams/${team.id}/members`, {
|
||||
data: {username: currentUser.username},
|
||||
headers: {Authorization: `Bearer ${tokenA}`},
|
||||
})
|
||||
|
||||
// Wait for unread indicator then click the bell
|
||||
await expect(page.locator('.notifications .unread-indicator')).toBeVisible({
|
||||
timeout: 10000,
|
||||
})
|
||||
await page.locator('.notifications .trigger-button').click()
|
||||
|
||||
// Notification dropdown should contain the team notification
|
||||
const notificationsList = page.locator('.notifications .notifications-list')
|
||||
await expect(notificationsList).toBeVisible()
|
||||
await expect(notificationsList.locator('.single-notification')).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('websocket disconnects on logout', async ({authenticatedPage: page}) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Perform logout — click user menu then logout button
|
||||
await page.locator('.navbar .username-dropdown-trigger').click()
|
||||
await page.locator('.navbar .dropdown-item').filter({hasText: 'Logout'}).click()
|
||||
|
||||
// After logout, should redirect to login page
|
||||
await expect(page).toHaveURL(/\/login/, {timeout: 5000})
|
||||
|
||||
// Verify the notification bell is gone (no authenticated UI)
|
||||
await expect(page.locator('.notifications .trigger-button')).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
import {test, expect} from '../../support/fixtures'
|
||||
import {openWs, waitForMessage, sendMessage, authenticateWs, subscribeWs, collectMessages, closeWs} from '../../support/websocket'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {TEST_PASSWORD} from '../../support/constants'
|
||||
import type {APIRequestContext} from '@playwright/test'
|
||||
|
||||
/** Login without setting page localStorage — just returns the token. */
|
||||
async function loginRaw(apiContext: APIRequestContext, user: {username: string}): Promise<{token: string}> {
|
||||
const response = await apiContext.post('login', {
|
||||
data: {username: user.username, password: TEST_PASSWORD},
|
||||
})
|
||||
return response.json()
|
||||
}
|
||||
|
||||
test.describe('WebSocket Protocol', () => {
|
||||
|
||||
test.describe('Authentication', () => {
|
||||
test('authenticates with valid token', async ({userToken}) => {
|
||||
const ws = await openWs()
|
||||
try {
|
||||
const msg = await authenticateWs(ws, userToken)
|
||||
expect(msg.action).toBe('auth.success')
|
||||
expect(msg.success).toBe(true)
|
||||
} finally {
|
||||
closeWs(ws)
|
||||
}
|
||||
})
|
||||
|
||||
test('rejects invalid token', async () => {
|
||||
const ws = await openWs()
|
||||
try {
|
||||
sendMessage(ws, {action: 'auth', token: 'invalid-token'})
|
||||
const msg = await waitForMessage(ws)
|
||||
expect(msg.error).toBe('invalid_token')
|
||||
} finally {
|
||||
closeWs(ws)
|
||||
}
|
||||
})
|
||||
|
||||
test('closes connection after auth timeout', async () => {
|
||||
test.setTimeout(45000)
|
||||
const ws = await openWs()
|
||||
const closed = new Promise<{code: number; reason: string}>((resolve) => {
|
||||
ws.on('close', (code, reason) => {
|
||||
resolve({code, reason: reason.toString()})
|
||||
})
|
||||
})
|
||||
const result = await closed
|
||||
// websocket StatusPolicyViolation = 1008
|
||||
expect(result.code).toBe(1008)
|
||||
})
|
||||
|
||||
test('rejects double authentication', async ({userToken}) => {
|
||||
const ws = await openWs()
|
||||
try {
|
||||
await authenticateWs(ws, userToken)
|
||||
sendMessage(ws, {action: 'auth', token: userToken})
|
||||
const msg = await waitForMessage(ws)
|
||||
expect(msg.error).toBe('already_authenticated')
|
||||
} finally {
|
||||
closeWs(ws)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Subscribe / Unsubscribe Events', () => {
|
||||
test('subscribes to valid event', async ({userToken}) => {
|
||||
const ws = await openWs()
|
||||
try {
|
||||
await authenticateWs(ws, userToken)
|
||||
sendMessage(ws, {action: 'subscribe', event: 'notification.created'})
|
||||
// No error response means success — verify by collecting messages
|
||||
// for a short window. If there was an error, it would arrive.
|
||||
const messages = await collectMessages(ws, 500)
|
||||
const errors = messages.filter(m => m.error)
|
||||
expect(errors).toHaveLength(0)
|
||||
} finally {
|
||||
closeWs(ws)
|
||||
}
|
||||
})
|
||||
|
||||
test('rejects invalid event', async ({userToken}) => {
|
||||
const ws = await openWs()
|
||||
try {
|
||||
await authenticateWs(ws, userToken)
|
||||
sendMessage(ws, {action: 'subscribe', event: 'nonexistent.event'})
|
||||
const msg = await waitForMessage(ws)
|
||||
expect(msg.error).toBe('invalid_event')
|
||||
expect(msg.event).toBe('nonexistent.event')
|
||||
} finally {
|
||||
closeWs(ws)
|
||||
}
|
||||
})
|
||||
|
||||
test('requires auth before subscribe', async () => {
|
||||
const ws = await openWs()
|
||||
try {
|
||||
sendMessage(ws, {action: 'subscribe', event: 'notification.created'})
|
||||
const msg = await waitForMessage(ws)
|
||||
expect(msg.error).toBe('auth_required')
|
||||
} finally {
|
||||
closeWs(ws)
|
||||
}
|
||||
})
|
||||
|
||||
test('unsubscribe stops receiving events', async ({apiContext, userToken, currentUser}) => {
|
||||
const ws = await openWs()
|
||||
try {
|
||||
await authenticateWs(ws, userToken)
|
||||
subscribeWs(ws, 'notification.created')
|
||||
|
||||
// Create a second user to trigger the notification
|
||||
const [userA] = await UserFactory.create(1, {id: 100}, false)
|
||||
const {token: tokenA} = await loginRaw(apiContext, userA)
|
||||
|
||||
// User A creates a team
|
||||
const teamResponse = await apiContext.put('teams', {
|
||||
data: {name: 'Test Team'},
|
||||
headers: {Authorization: `Bearer ${tokenA}`},
|
||||
})
|
||||
const team = await teamResponse.json()
|
||||
|
||||
// Unsubscribe before the notification is triggered
|
||||
sendMessage(ws, {action: 'unsubscribe', event: 'notification.created'})
|
||||
// Give the server a moment to process the unsubscribe
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
|
||||
// Now add currentUser to team — should NOT receive WS notification
|
||||
await apiContext.put(`teams/${team.id}/members`, {
|
||||
data: {username: currentUser.username},
|
||||
headers: {Authorization: `Bearer ${tokenA}`},
|
||||
})
|
||||
|
||||
// Collect messages for 2 seconds — should get none
|
||||
const messages = await collectMessages(ws, 2000)
|
||||
const notifications = messages.filter(m => m.event === 'notification.created')
|
||||
expect(notifications).toHaveLength(0)
|
||||
} finally {
|
||||
closeWs(ws)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Message Delivery', () => {
|
||||
test('receives notification when added to team', async ({apiContext, userToken, currentUser}) => {
|
||||
const ws = await openWs()
|
||||
try {
|
||||
await authenticateWs(ws, userToken)
|
||||
subscribeWs(ws, 'notification.created')
|
||||
|
||||
// Create a second user (the doer)
|
||||
const [userA] = await UserFactory.create(1, {id: 100}, false)
|
||||
const {token: tokenA} = await loginRaw(apiContext, userA)
|
||||
|
||||
// User A creates a team
|
||||
const teamResponse = await apiContext.put('teams', {
|
||||
data: {name: 'Notification Test Team'},
|
||||
headers: {Authorization: `Bearer ${tokenA}`},
|
||||
})
|
||||
const team = await teamResponse.json()
|
||||
|
||||
// User A adds currentUser to the team
|
||||
const addResponse = await apiContext.put(`teams/${team.id}/members`, {
|
||||
data: {username: currentUser.username},
|
||||
headers: {Authorization: `Bearer ${tokenA}`},
|
||||
})
|
||||
expect(addResponse.ok()).toBe(true)
|
||||
|
||||
// currentUser should receive the notification via WebSocket
|
||||
const msg = await waitForMessage(ws, 10000)
|
||||
expect(msg.event).toBe('notification.created')
|
||||
expect(msg.data).toBeDefined()
|
||||
} finally {
|
||||
closeWs(ws)
|
||||
}
|
||||
})
|
||||
|
||||
test('doer does not receive own notification', async ({apiContext, userToken}) => {
|
||||
const ws = await openWs()
|
||||
try {
|
||||
await authenticateWs(ws, userToken)
|
||||
subscribeWs(ws, 'notification.created')
|
||||
|
||||
// Create a second user
|
||||
const [otherUser] = await UserFactory.create(1, {id: 100}, false)
|
||||
|
||||
// currentUser creates a team (they are the doer)
|
||||
const teamResponse = await apiContext.put('teams', {
|
||||
data: {name: 'Doer Test Team'},
|
||||
headers: {Authorization: `Bearer ${userToken}`},
|
||||
})
|
||||
const team = await teamResponse.json()
|
||||
|
||||
// currentUser adds otherUser — currentUser is the doer
|
||||
await apiContext.put(`teams/${team.id}/members`, {
|
||||
data: {username: otherUser.username},
|
||||
headers: {Authorization: `Bearer ${userToken}`},
|
||||
})
|
||||
|
||||
// currentUser should NOT receive a notification (they did the action)
|
||||
const messages = await collectMessages(ws, 3000)
|
||||
const notifications = messages.filter(m => m.event === 'notification.created')
|
||||
expect(notifications).toHaveLength(0)
|
||||
} finally {
|
||||
closeWs(ws)
|
||||
}
|
||||
})
|
||||
|
||||
test('multiple connections receive same notification', async ({apiContext, userToken, currentUser}) => {
|
||||
const ws1 = await openWs()
|
||||
const ws2 = await openWs()
|
||||
try {
|
||||
// Both connections authenticate as the same user
|
||||
await authenticateWs(ws1, userToken)
|
||||
await authenticateWs(ws2, userToken)
|
||||
subscribeWs(ws1, 'notification.created')
|
||||
subscribeWs(ws2, 'notification.created')
|
||||
|
||||
// Create a second user to trigger notification
|
||||
const [userA] = await UserFactory.create(1, {id: 100}, false)
|
||||
const {token: tokenA} = await loginRaw(apiContext, userA)
|
||||
|
||||
const teamResponse = await apiContext.put('teams', {
|
||||
data: {name: 'Multi-Connection Team'},
|
||||
headers: {Authorization: `Bearer ${tokenA}`},
|
||||
})
|
||||
const team = await teamResponse.json()
|
||||
|
||||
await apiContext.put(`teams/${team.id}/members`, {
|
||||
data: {username: currentUser.username},
|
||||
headers: {Authorization: `Bearer ${tokenA}`},
|
||||
})
|
||||
|
||||
// Both connections should receive the notification
|
||||
const [msg1, msg2] = await Promise.all([
|
||||
waitForMessage(ws1, 10000),
|
||||
waitForMessage(ws2, 10000),
|
||||
])
|
||||
expect(msg1.event).toBe('notification.created')
|
||||
expect(msg2.event).toBe('notification.created')
|
||||
} finally {
|
||||
closeWs(ws1)
|
||||
closeWs(ws2)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import WebSocket from 'ws'
|
||||
|
||||
const API_URL = process.env.API_URL || 'http://localhost:3456/api/v1'
|
||||
|
||||
export interface WsMessage {
|
||||
event?: string
|
||||
action?: string
|
||||
success?: boolean
|
||||
error?: string
|
||||
data?: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the WebSocket URL derived from the API base URL.
|
||||
*/
|
||||
export function getWsUrl(): string {
|
||||
return API_URL.replace(/\/+$/, '').replace(/^http/, 'ws') + '/ws'
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a raw WebSocket connection to the API.
|
||||
*/
|
||||
export function openWs(): Promise<WebSocket> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(getWsUrl())
|
||||
ws.on('open', () => resolve(ws))
|
||||
ws.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the next message on a WebSocket connection.
|
||||
*/
|
||||
export function waitForMessage(ws: WebSocket, timeout = 5000): Promise<WsMessage> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('WebSocket message timeout')), timeout)
|
||||
ws.once('message', (data) => {
|
||||
clearTimeout(timer)
|
||||
resolve(JSON.parse(data.toString()))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a JSON message on the WebSocket.
|
||||
*/
|
||||
export function sendMessage(ws: WebSocket, msg: object): void {
|
||||
ws.send(JSON.stringify(msg))
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a WebSocket connection and returns the auth.success message.
|
||||
*/
|
||||
export async function authenticateWs(ws: WebSocket, token: string): Promise<WsMessage> {
|
||||
sendMessage(ws, {action: 'auth', token})
|
||||
const msg = await waitForMessage(ws)
|
||||
if (msg.action !== 'auth.success') {
|
||||
throw new Error(`Expected auth.success, got: ${JSON.stringify(msg)}`)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to an event on an authenticated WebSocket connection.
|
||||
*/
|
||||
export function subscribeWs(ws: WebSocket, event: string): void {
|
||||
sendMessage(ws, {action: 'subscribe', event})
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all messages received within a time window.
|
||||
*/
|
||||
export function collectMessages(ws: WebSocket, duration: number): Promise<WsMessage[]> {
|
||||
return new Promise((resolve) => {
|
||||
const messages: WsMessage[] = []
|
||||
const handler = (data: WebSocket.Data) => {
|
||||
messages.push(JSON.parse(data.toString()))
|
||||
}
|
||||
ws.on('message', handler)
|
||||
setTimeout(() => {
|
||||
ws.off('message', handler)
|
||||
resolve(messages)
|
||||
}, duration)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a WebSocket connection safely.
|
||||
*/
|
||||
export function closeWs(ws: WebSocket): void {
|
||||
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
||||
ws.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,24 @@ func HandleTesting(c *echo.Context) error {
|
|||
|
||||
truncate := c.QueryParam("truncate")
|
||||
if truncate == "true" || truncate == "" {
|
||||
// When truncating certain tables, also truncate dependent tables
|
||||
// whose rows reference the truncated table by user/entity ID.
|
||||
// Without foreign key cascades, stale rows would persist and
|
||||
// pollute subsequent tests that reuse the same auto-increment IDs.
|
||||
dependentTables := map[string][]string{
|
||||
"users": {"notifications"},
|
||||
}
|
||||
if deps, ok := dependentTables[table]; ok {
|
||||
for _, dep := range deps {
|
||||
if err = db.RestoreAndTruncate(dep, nil); err != nil {
|
||||
log.Errorf("Error truncating dependent table %s: %v", dep, err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
|
||||
"error": true,
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
err = db.RestoreAndTruncate(table, content)
|
||||
} else {
|
||||
err = db.Restore(table, content)
|
||||
|
|
|
|||
Loading…
Reference in New Issue