tests: add e2e LDAP tests (#466)

Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Kyle Mendell
2025-05-07 09:38:02 -05:00
committed by GitHub
parent ba256c76bc
commit 73c82ae43a
7 changed files with 309 additions and 47 deletions

View File

@@ -72,6 +72,23 @@ jobs:
- name: Load Docker image - name: Load Docker image
run: docker load -i /tmp/docker-image.tar run: docker load -i /tmp/docker-image.tar
- name: Cache LLDAP Docker image
uses: actions/cache@v3
id: lldap-cache
with:
path: /tmp/lldap-image.tar
key: lldap-stable-${{ runner.os }}
- name: Pull and save LLDAP image
if: steps.lldap-cache.outputs.cache-hit != 'true'
run: |
docker pull nitnelave/lldap:stable
docker save nitnelave/lldap:stable > /tmp/lldap-image.tar
- name: Load LLDAP image from cache
if: steps.lldap-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/lldap-image.tar
- name: Install frontend dependencies - name: Install frontend dependencies
working-directory: ./frontend working-directory: ./frontend
run: npm ci run: npm ci
@@ -81,15 +98,27 @@ jobs:
if: steps.playwright-cache.outputs.cache-hit != 'true' if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium run: npx playwright install --with-deps chromium
- name: Run Docker Container with Sqlite DB - name: Create Docker network
run: docker network create pocket-id-network
- name: Setup and Configure LLDAP Server
run: |
chmod +x ./scripts/tests/setup-lldap.sh
./scripts/tests/setup-lldap.sh
- name: Run Docker Container with Sqlite DB and LDAP
run: | run: |
docker run -d --name pocket-id-sqlite \ docker run -d --name pocket-id-sqlite \
--network pocket-id-network \
-p 80:80 \ -p 80:80 \
-e APP_ENV=test \ -e APP_ENV=test \
pocket-id:test pocket-id:test
docker logs -f pocket-id-sqlite &> /tmp/backend.log & docker logs -f pocket-id-sqlite &> /tmp/backend.log &
- name: Wait for backend to sync LDAP data
run: sleep 10
- name: Run Playwright tests - name: Run Playwright tests
working-directory: ./frontend working-directory: ./frontend
run: npx playwright test run: npx playwright test
@@ -150,6 +179,23 @@ jobs:
if: steps.postgres-cache.outputs.cache-hit == 'true' if: steps.postgres-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/postgres-image.tar run: docker load < /tmp/postgres-image.tar
- name: Cache LLDAP Docker image
uses: actions/cache@v3
id: lldap-cache
with:
path: /tmp/lldap-image.tar
key: lldap-stable-${{ runner.os }}
- name: Pull and save LLDAP image
if: steps.lldap-cache.outputs.cache-hit != 'true'
run: |
docker pull nitnelave/lldap:stable
docker save nitnelave/lldap:stable > /tmp/lldap-image.tar
- name: Load LLDAP image from cache
if: steps.lldap-cache.outputs.cache-hit == 'true'
run: docker load < /tmp/lldap-image.tar
- name: Download Docker image artifact - name: Download Docker image artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -192,7 +238,12 @@ jobs:
sleep 2 sleep 2
done done
- name: Run Docker Container with Postgres DB - name: Setup and Configure LLDAP Server
run: |
chmod +x ./scripts/tests/setup-lldap.sh
./scripts/tests/setup-lldap.sh
- name: Run Docker Container with Postgres DB and LDAP
run: | run: |
docker run -d --name pocket-id-postgres \ docker run -d --name pocket-id-postgres \
--network pocket-id-network \ --network pocket-id-network \
@@ -204,6 +255,9 @@ jobs:
docker logs -f pocket-id-postgres &> /tmp/backend.log & docker logs -f pocket-id-postgres &> /tmp/backend.log &
- name: Wait for backend to sync LDAP data
run: sleep 10
- name: Run Playwright tests - name: Run Playwright tests
working-directory: ./frontend working-directory: ./frontend
run: npx playwright test run: npx playwright test

View File

@@ -14,7 +14,7 @@ import (
func init() { func init() {
registerTestControllers = []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services){ registerTestControllers = []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services){
func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) { func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) {
testService := service.NewTestService(db, svc.appConfigService, svc.jwtService) testService := service.NewTestService(db, svc.appConfigService, svc.jwtService, svc.ldapService)
controller.NewTestController(apiGroup, testService) controller.NewTestController(apiGroup, testService)
}, },
} }

View File

@@ -41,6 +41,16 @@ func (tc *TestController) resetAndSeedHandler(c *gin.Context) {
return return
} }
if err := tc.TestService.SetLdapTestConfig(c.Request.Context()); err != nil {
_ = c.Error(err)
return
}
if err := tc.TestService.SyncLdap(c.Request.Context()); err != nil {
_ = c.Error(err)
return
}
tc.TestService.SetJWTKeys() tc.TestService.SetJWTKeys()
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)

View File

@@ -29,15 +29,16 @@ type TestService struct {
db *gorm.DB db *gorm.DB
jwtService *JwtService jwtService *JwtService
appConfigService *AppConfigService appConfigService *AppConfigService
ldapService *LdapService
} }
func NewTestService(db *gorm.DB, appConfigService *AppConfigService, jwtService *JwtService) *TestService { func NewTestService(db *gorm.DB, appConfigService *AppConfigService, jwtService *JwtService, ldapService *LdapService) *TestService {
return &TestService{db: db, appConfigService: appConfigService, jwtService: jwtService} return &TestService{db: db, appConfigService: appConfigService, jwtService: jwtService, ldapService: ldapService}
} }
//nolint:gocognit //nolint:gocognit
func (s *TestService) SeedDatabase() error { func (s *TestService) SeedDatabase() error {
return s.db.Transaction(func(tx *gorm.DB) error { err := s.db.Transaction(func(tx *gorm.DB) error {
users := []model.User{ users := []model.User{
{ {
Base: model.Base{ Base: model.Base{
@@ -238,6 +239,12 @@ func (s *TestService) SeedDatabase() error {
return nil return nil
}) })
if err != nil {
return err
}
return nil
} }
func (s *TestService) ResetDatabase() error { func (s *TestService) ResetDatabase() error {
@@ -349,3 +356,52 @@ func (s *TestService) getCborPublicKey(base64PublicKey string) ([]byte, error) {
return cborPublicKey, nil return cborPublicKey, nil
} }
// SyncLdap triggers an LDAP synchronization
func (s *TestService) SyncLdap(ctx context.Context) error {
return s.ldapService.SyncAll(ctx)
}
// SetLdapTestConfig writes the test LDAP config variables directly to the database.
func (s *TestService) SetLdapTestConfig(ctx context.Context) error {
err := s.db.Transaction(func(tx *gorm.DB) error {
ldapConfigs := map[string]string{
"ldapUrl": "ldap://lldap:3890",
"ldapBindDn": "uid=admin,ou=people,dc=pocket-id,dc=org",
"ldapBindPassword": "admin_password",
"ldapBase": "dc=pocket-id,dc=org",
"ldapUserSearchFilter": "(objectClass=person)",
"ldapUserGroupSearchFilter": "(objectClass=groupOfNames)",
"ldapSkipCertVerify": "true",
"ldapAttributeUserUniqueIdentifier": "uuid",
"ldapAttributeUserUsername": "uid",
"ldapAttributeUserEmail": "mail",
"ldapAttributeUserFirstName": "givenName",
"ldapAttributeUserLastName": "sn",
"ldapAttributeGroupUniqueIdentifier": "uuid",
"ldapAttributeGroupName": "uid",
"ldapAttributeGroupMember": "member",
"ldapAttributeAdminGroup": "admin_group",
"ldapSoftDeleteUsers": "true",
"ldapEnabled": "true",
}
for key, value := range ldapConfigs {
configVar := model.AppConfigVariable{Key: key, Value: value}
if err := tx.Create(&configVar).Error; err != nil {
return fmt.Errorf("failed to create config variable '%s': %w", key, err)
}
}
return nil
})
if err != nil {
return fmt.Errorf("failed to set LDAP test config: %w", err)
}
if err := s.appConfigService.LoadDbConfig(ctx); err != nil {
return fmt.Errorf("failed to load app config: %w", err)
}
return nil
}

View File

@@ -53,47 +53,6 @@ test('Update email configuration', async ({ page }) => {
await expect(page.getByLabel('API Key Expiration')).toBeChecked(); await expect(page.getByLabel('API Key Expiration')).toBeChecked();
}); });
test('Update LDAP configuration', async ({ page }) => {
await page.goto('/settings/admin/application-configuration');
await page.getByRole('button', { name: 'Expand card' }).nth(2).click();
await page.getByLabel('LDAP URL').fill('ldap://localhost:389');
await page.getByLabel('LDAP Bind DN').fill('cn=admin,dc=example,dc=com');
await page.getByLabel('LDAP Bind Password').fill('password');
await page.getByLabel('LDAP Base DN').fill('dc=example,dc=com');
await page.getByLabel('User Search Filter').fill('(objectClass=person)');
await page.getByLabel('Groups Search Filter').fill('(objectClass=groupOfUniqueNames)');
await page.getByLabel('User Unique Identifier Attribute').fill('uuid');
await page.getByLabel('Username Attribute').fill('uid');
await page.getByLabel('User Mail Attribute').fill('mail');
await page.getByLabel('User First Name Attribute').fill('givenName');
await page.getByLabel('User Last Name Attribute').fill('sn');
await page.getByLabel('Group Unique Identifier Attribute').fill('uuid');
await page.getByLabel('Group Name Attribute').fill('cn');
await page.getByLabel('Admin Group Name').fill('admin');
await page.getByRole('button', { name: 'Enable' }).click();
await expect(page.getByRole('status')).toHaveText('LDAP configuration updated successfully');
await page.reload();
await expect(page.getByRole('button', { name: 'Disable' })).toBeVisible();
await expect(page.getByLabel('LDAP URL')).toHaveValue('ldap://localhost:389');
await expect(page.getByLabel('LDAP Bind DN')).toHaveValue('cn=admin,dc=example,dc=com');
await expect(page.getByLabel('LDAP Bind Password')).toHaveValue('password');
await expect(page.getByLabel('LDAP Base DN')).toHaveValue('dc=example,dc=com');
await page.getByLabel('User Search Filter').fill('(objectClass=person)');
await page.getByLabel('Groups Search Filter').fill('(objectClass=groupOfUniqueNames)');
await expect(page.getByLabel('User Unique Identifier Attribute')).toHaveValue('uuid');
await expect(page.getByLabel('Username Attribute')).toHaveValue('uid');
await expect(page.getByLabel('User Mail Attribute')).toHaveValue('mail');
await expect(page.getByLabel('User First Name Attribute')).toHaveValue('givenName');
await expect(page.getByLabel('User Last Name Attribute')).toHaveValue('sn');
await expect(page.getByLabel('Admin Group Name')).toHaveValue('admin');
});
test('Update application images', async ({ page }) => { test('Update application images', async ({ page }) => {
await page.goto('/settings/admin/application-configuration'); await page.goto('/settings/admin/application-configuration');

View File

@@ -0,0 +1,83 @@
import test, { expect } from '@playwright/test';
import { cleanupBackend } from './utils/cleanup.util';
test.beforeEach(cleanupBackend);
test.describe('LDAP Integration', () => {
test('LDAP configuration is working properly', async ({ page }) => {
await page.goto('/settings/admin/application-configuration');
await page.getByRole('heading', { name: 'LDAP' }).click();
await expect(page.getByRole('button', { name: 'Disable' })).toBeVisible();
await expect(page.getByLabel('LDAP URL')).toHaveValue(/ldap:\/\/.*/);
await expect(page.getByLabel('LDAP Base DN')).not.toBeEmpty();
await expect(page.getByLabel('User Unique Identifier Attribute')).not.toBeEmpty();
await expect(page.getByLabel('Username Attribute')).not.toBeEmpty();
await expect(page.getByLabel('User Mail Attribute')).not.toBeEmpty();
await expect(page.getByLabel('Group Name Attribute')).not.toBeEmpty();
const syncButton = page.getByRole('button', { name: 'Sync now' });
await syncButton.click();
await expect(page.getByText('LDAP sync finished')).toBeVisible();
});
test('LDAP users are synced into PocketID', async ({ page }) => {
// Navigate to user management
await page.goto('/settings/admin/users');
// Verify the LDAP users exist
await expect(page.getByText('testuser1@pocket-id.org')).toBeVisible();
await expect(page.getByText('testuser2@pocket-id.org')).toBeVisible();
// Check LDAP user details
await page.getByRole('row', { name: 'testuser1' }).getByRole('button').click();
await page.getByRole('menuitem', { name: 'Edit' }).click();
// Verify user source is LDAP
await expect(page.getByText('LDAP').first()).toBeVisible();
// Verify essential fields are filled
await expect(page.getByLabel('Username')).not.toBeEmpty();
await expect(page.getByLabel('Email')).not.toBeEmpty();
});
test('LDAP groups are synced into PocketID', async ({ page }) => {
// Navigate to user groups
await page.goto('/settings/admin/user-groups');
// Verify LDAP groups exist
await expect(page.getByRole('cell', { name: 'test_group' }).first()).toBeVisible();
await expect(page.getByRole('cell', { name: 'admin_group' }).first()).toBeVisible();
// Check group details
await page.getByRole('row', { name: 'test_group' }).getByRole('button').click();
await page.getByRole('menuitem', { name: 'Edit' }).click();
// Verify group source is LDAP
await expect(page.getByText('LDAP').first()).toBeVisible();
});
test('LDAP users cannot be modified in PocketID', async ({ page }) => {
// Navigate to LDAP user details
await page.goto('/settings/admin/users');
await page.getByRole('row', { name: 'testuser1' }).getByRole('button').click();
await page.getByRole('menuitem', { name: 'Edit' }).click();
// Verify key fields are disabled
const usernameInput = page.getByLabel('Username');
await expect(usernameInput).toBeDisabled();
});
test('LDAP groups cannot be modified in PocketID', async ({ page }) => {
// Navigate to LDAP group details
await page.goto('/settings/admin/user-groups');
await page.getByRole('row', { name: 'test_group' }).getByRole('button').click();
await page.getByRole('menuitem', { name: 'Edit' }).click();
// Verify key fields are disabled
const nameInput = page.getByLabel('Name', { exact: true });
await expect(nameInput).toBeDisabled();
});
});

View File

@@ -0,0 +1,100 @@
#!/bin/bash
set -e
echo 'deb http://download.opensuse.org/repositories/home:/Masgalor:/LLDAP/xUbuntu_24.04/ /' | sudo tee /etc/apt/sources.list.d/home:Masgalor:LLDAP.list
curl -fsSL https://download.opensuse.org/repositories/home:Masgalor:LLDAP/xUbuntu_24.04/Release.key | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/home_Masgalor_LLDAP.gpg > /dev/null
sudo apt-get update
sudo apt-get install -y lldap-cli lldap-set-password
echo "Setting up LLDAP container..."
# Run LLDAP container
docker run -d --name lldap \
--network pocket-id-network \
-p 3890:3890 \
-p 17170:17170 \
-e LLDAP_JWT_SECRET=secret \
-e LLDAP_LDAP_USER_PASS=admin_password \
-e LLDAP_LDAP_BASE_DN="dc=pocket-id,dc=org" \
nitnelave/lldap:stable
# Wait for LLDAP to start
for i in {1..15}; do
if curl -s --fail http://localhost:17170/api/healthcheck > /dev/null; then
echo "LLDAP is ready"
break
fi
if [ $i -eq 15 ]; then
echo "LLDAP failed to start in time"
exit 1
fi
echo "Waiting for LLDAP... ($i/15)"
sleep 3
done
echo "LLDAP container setup complete"
echo "Setting up LLDAP test data..."
# Configure LLDAP CLI connection via environment variables
export LLDAP_HTTPURL="http://localhost:17170"
export LLDAP_USERNAME="admin"
export LLDAP_PASSWORD="admin_password"
# Create test users using the user add command
echo "Creating test users..."
lldap-cli user add "testuser1" "testuser1@pocket-id.org" \
-p "password123" \
-d "Test User 1" \
-f "Test" \
-l "User"
lldap-cli user add "testuser2" "testuser2@pocket-id.org" \
-p "password123" \
-d "Test User 2" \
-f "Test2" \
-l "User2"
# Create test groups
echo "Creating test groups..."
lldap-cli group add "test_group"
sleep 1
lldap-cli group update set "test_group" "display_name" "test_group"
lldap-cli group add "admin_group"
sleep 1
lldap-cli group update set "admin_group" "display_name" "admin_group"
# Add users to groups with retry logic
echo "Adding users to groups..."
for i in {1..3}; do
echo "Attempt $i to add testuser1 to test_group"
if lldap-cli user group add "testuser1" "test_group"; then
echo "Successfully added testuser1 to test_group"
break
else
echo "Failed to add testuser1 to test_group, retrying in 2 seconds..."
sleep 2
fi
if [ $i -eq 3 ]; then
echo "Warning: Could not add testuser1 to test_group after 3 attempts"
fi
done
for i in {1..3}; do
echo "Attempt $i to add testuser2 to admin_group"
if lldap-cli user group add "testuser2" "admin_group"; then
echo "Successfully added testuser2 to admin_group"
break
else
echo "Failed to add testuser2 to admin_group, retrying in 2 seconds..."
sleep 2
fi
if [ $i -eq 3 ]; then
echo "Warning: Could not add testuser2 to admin_group after 3 attempts"
fi
done
echo "LLDAP test data setup complete"