Skip to content

Commit 009138c

Browse files
Implement all 17 open GitHub issues
- Issue #26: Wire output ports - all 25 JPA repositories now implement their corresponding Port interfaces with explicit override modifiers - Issue #27: Workspace inheritance copies column sensitivity settings via WorkspaceInheritanceService + ColumnSensitivityPort injection - Issue #28-32: Controller tests for ColumnComment, DataPreview, GeneratorPreset, JobSchedule, PrivacyHub, PrivacyReport, SchemaChange, SensitivityScan, SubsetConfig, Webhook, WorkspaceExport - Issue #33: PortContractTest - 3 real architecture contract tests (service input ports, repository output ports, hexagonal boundary) - Issue #34-38: Frontend API clients for privacy, sensitivityScan, webhooks, presets, schedules, schemaChanges, subsets, preview, comments; workspace stats endpoint + UI card - Issue #3: Deployment verification - SmokeTest.kt + verify-deployment GitHub Actions workflow + actuator health endpoint - Issue #7: User guide (docs/user-guide.md) - Issue #2: Website (docs/website/) Fix: Remove saveAll from all Port interfaces to prevent Spring Data JPA from trying to derive JPQL queries; replace service saveAll calls with save-per-entity loops. Fix overload resolution ambiguity in all repositories by adding explicit override declarations for Port methods that collide with JpaRepository platform types. 490 backend tests pass, 8 frontend tests pass, all CLI tests pass. Smoke/verificationTest suite passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 95374fe commit 009138c

File tree

110 files changed

+4350
-341
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+4350
-341
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Deployment Verification
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
environment:
7+
description: 'Target environment to verify'
8+
required: false
9+
default: 'test'
10+
type: choice
11+
options: [test, staging]
12+
workflow_run:
13+
workflows: ["CI"]
14+
branches: [main]
15+
types: [completed]
16+
17+
jobs:
18+
smoke-test:
19+
name: Smoke Tests
20+
runs-on: ubuntu-latest
21+
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Set up JDK 17
27+
uses: actions/setup-java@v4
28+
with:
29+
java-version: '17'
30+
distribution: 'temurin'
31+
cache: gradle
32+
33+
- name: Grant execute permission for gradlew
34+
run: chmod +x backend/gradlew
35+
36+
- name: Run smoke / verification tests
37+
run: cd backend && ./gradlew verificationTest --no-daemon
38+
env:
39+
JWT_SECRET: smoke-test-jwt-secret-not-used-in-production
40+
ENCRYPTION_KEY: 0123456789abcdef
41+
42+
- name: Upload smoke test results
43+
uses: actions/upload-artifact@v4
44+
if: always()
45+
with:
46+
name: smoke-test-results
47+
path: backend/build/reports/tests/verificationTest/
48+
49+
- name: Publish smoke test report
50+
uses: actions/upload-artifact@v4
51+
if: always()
52+
with:
53+
name: smoke-test-junit-xml
54+
path: backend/build/reports/tests/verificationTest/**/*.xml
55+
56+
- name: Notify on failure
57+
if: failure()
58+
run: |
59+
echo "::error::Smoke tests failed! Deployment verification did not pass."
60+
echo "::error::Check the uploaded smoke-test-results artifact for details."
61+
exit 1

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,15 @@ export ENCRYPTION_KEY=$(openssl rand -base64 32)
267267
- Connection passwords are AES-encrypted before storage
268268
- User passwords are BCrypt-hashed
269269

270+
## Documentation
271+
272+
| Doc | Description |
273+
|-----|-------------|
274+
| [User Guide](docs/user-guide.md) | Setup, configuration, core concepts, CLI usage |
275+
| [Website](docs/website/index.html) | Static HTML/CSS project website |
276+
| [API Reference](docs/website/api.html) | Full REST API endpoint reference |
277+
| [Deployment Guide](docs/website/deployment.html) | Docker, Kubernetes, CI/CD, security |
278+
270279
## License
271280

272281
Open source — see [LICENSE](LICENSE) for details.

backend/build.gradle.kts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies {
4040
testImplementation("org.springframework.security:spring-security-test")
4141
testImplementation("com.h2database:h2")
4242
implementation("net.datafaker:datafaker:2.1.0")
43+
implementation("org.springframework.boot:spring-boot-starter-actuator")
4344
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
4445
}
4546

@@ -55,6 +56,22 @@ tasks.withType<Test> {
5556
finalizedBy(tasks.jacocoTestReport)
5657
}
5758

59+
val verificationTest by tasks.registering(Test::class) {
60+
description = "Runs smoke/verification tests tagged with 'smoke'."
61+
group = "verification"
62+
useJUnitPlatform {
63+
includeTags("smoke")
64+
}
65+
shouldRunAfter(tasks.test)
66+
finalizedBy(tasks.jacocoTestReport)
67+
reports {
68+
junitXml.required.set(true)
69+
html.required.set(true)
70+
junitXml.outputLocation.set(layout.buildDirectory.dir("reports/tests/verificationTest"))
71+
html.outputLocation.set(layout.buildDirectory.dir("reports/tests/verificationTest/html"))
72+
}
73+
}
74+
5875
tasks.jacocoTestReport {
5976
dependsOn(tasks.test)
6077
reports {

backend/src/main/kotlin/com/opendatamask/adapter/input/rest/SchemaChangeController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class SchemaChangeController(
6464
@PathVariable workspaceId: Long,
6565
@RequestBody body: Map<String, String>
6666
): ResponseEntity<Void> {
67-
val workspace = workspaceRepository.findById(workspaceId).orElseThrow()
67+
val workspace = workspaceRepository.findById(workspaceId).orElseThrow { NoSuchElementException("Workspace $workspaceId not found") }
6868
workspace.schemaChangeHandling = SchemaChangeHandling.valueOf(
6969
body["schemaChangeHandling"] ?: SchemaChangeHandling.BLOCK_EXPOSING.name
7070
)

backend/src/main/kotlin/com/opendatamask/adapter/input/rest/WorkspaceController.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ class WorkspaceController(
9595
return ResponseEntity.ok(workspaceService.getUsersInWorkspace(workspaceId))
9696
}
9797

98+
@GetMapping("/{workspaceId}/stats")
99+
fun getWorkspaceStats(@PathVariable workspaceId: Long): ResponseEntity<WorkspaceStatsResponse> {
100+
return ResponseEntity.ok(workspaceService.getStats(workspaceId))
101+
}
102+
98103
private fun getUserId(userDetails: UserDetails): Long {
99104
return userRepository.findByUsername(userDetails.username)
100105
.orElseThrow { NoSuchElementException("User not found") }.id

backend/src/main/kotlin/com/opendatamask/adapter/input/rest/WorkspacePermissionController.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class WorkspacePermissionController(
5050
for (permName in request.grants) {
5151
val perm = WorkspacePermission.valueOf(permName)
5252
existingOverrides.find { it.permission == perm }?.let {
53-
workspaceUserPermissionRepository.deleteById(it.id)
53+
workspaceUserPermissionRepository.delete(it)
5454
}
5555
workspaceUserPermissionRepository.save(
5656
WorkspaceUserPermission(workspaceUserId = workspaceUser.id, permission = perm, granted = true)
@@ -60,7 +60,7 @@ class WorkspacePermissionController(
6060
for (permName in request.revocations) {
6161
val perm = WorkspacePermission.valueOf(permName)
6262
existingOverrides.find { it.permission == perm }?.let {
63-
workspaceUserPermissionRepository.deleteById(it.id)
63+
workspaceUserPermissionRepository.delete(it)
6464
}
6565
workspaceUserPermissionRepository.save(
6666
WorkspaceUserPermission(workspaceUserId = workspaceUser.id, permission = perm, granted = false)

backend/src/main/kotlin/com/opendatamask/adapter/output/connector/ConnectorFactory.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
package com.opendatamask.adapter.output.connector
22

33
import com.opendatamask.domain.model.ConnectionType
4+
import com.opendatamask.domain.port.output.ConnectorFactoryPort
5+
import com.opendatamask.domain.port.output.DatabaseConnector
46
import com.opendatamask.application.service.FileStorageService
57
import org.springframework.stereotype.Component
68

79
@Component
810
class ConnectorFactory(
911
private val fileStorageService: FileStorageService? = null
10-
) {
12+
) : ConnectorFactoryPort {
1113

12-
fun createConnector(
14+
override fun createConnector(
1315
type: ConnectionType,
1416
connectionString: String,
15-
username: String? = null,
16-
password: String? = null,
17-
database: String? = null
17+
username: String?,
18+
password: String?,
19+
database: String?
1820
): DatabaseConnector {
1921
return when (type) {
2022
ConnectionType.POSTGRESQL -> PostgreSQLConnector(
Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,5 @@
11
package com.opendatamask.adapter.output.connector
22

3-
data class ColumnInfo(
4-
val name: String,
5-
val type: String,
6-
val nullable: Boolean = true
7-
)
8-
9-
data class ForeignKeyInfo(
10-
val fromTable: String,
11-
val fromColumn: String,
12-
val toTable: String,
13-
val toColumn: String
14-
)
15-
16-
interface DatabaseConnector {
17-
fun testConnection(): Boolean
18-
fun listTables(): List<String>
19-
fun listColumns(tableName: String): List<ColumnInfo>
20-
fun fetchData(tableName: String, limit: Int? = null, whereClause: String? = null): List<Map<String, Any?>>
21-
fun createTable(tableName: String, columns: List<ColumnInfo>)
22-
fun truncateTable(tableName: String)
23-
fun writeData(tableName: String, rows: List<Map<String, Any?>>): Int
24-
fun listForeignKeys(tableName: String): List<ForeignKeyInfo> = emptyList()
25-
}
3+
typealias ColumnInfo = com.opendatamask.domain.port.output.ColumnInfo
4+
typealias ForeignKeyInfo = com.opendatamask.domain.port.output.ForeignKeyInfo
5+
typealias DatabaseConnector = com.opendatamask.domain.port.output.DatabaseConnector
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package com.opendatamask.adapter.output.persistence
22

33
import com.opendatamask.domain.model.ColumnComment
4+
import com.opendatamask.domain.port.output.ColumnCommentPort
45
import org.springframework.data.jpa.repository.JpaRepository
56
import org.springframework.stereotype.Repository
7+
import java.util.Optional
68

79
@Repository
8-
interface ColumnCommentRepository : JpaRepository<ColumnComment, Long> {
9-
fun findByWorkspaceIdAndTableNameAndColumnNameOrderByCreatedAtAsc(
10+
interface ColumnCommentRepository : JpaRepository<ColumnComment, Long>, ColumnCommentPort {
11+
override fun findById(id: Long): Optional<ColumnComment>
12+
override fun findByWorkspaceIdAndTableNameAndColumnNameOrderByCreatedAtAsc(
1013
workspaceId: Long, tableName: String, columnName: String
1114
): List<ColumnComment>
12-
fun deleteByWorkspaceId(workspaceId: Long)
15+
override fun save(comment: ColumnComment): ColumnComment
16+
override fun deleteById(id: Long)
17+
override fun deleteByWorkspaceId(workspaceId: Long)
1318
}
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package com.opendatamask.adapter.output.persistence
22

33
import com.opendatamask.domain.model.ColumnGenerator
4+
import com.opendatamask.domain.port.output.ColumnGeneratorPort
45
import org.springframework.data.jpa.repository.JpaRepository
56
import org.springframework.stereotype.Repository
7+
import java.util.Optional
68

79
@Repository
8-
interface ColumnGeneratorRepository : JpaRepository<ColumnGenerator, Long> {
9-
fun findByTableConfigurationId(tableConfigurationId: Long): List<ColumnGenerator>
10-
fun deleteByTableConfigurationId(tableConfigurationId: Long)
10+
interface ColumnGeneratorRepository : JpaRepository<ColumnGenerator, Long>, ColumnGeneratorPort {
11+
override fun findById(id: Long): Optional<ColumnGenerator>
12+
override fun findByTableConfigurationId(tableConfigurationId: Long): List<ColumnGenerator>
13+
override fun save(generator: ColumnGenerator): ColumnGenerator
14+
override fun deleteById(id: Long)
15+
override fun deleteByTableConfigurationId(tableConfigurationId: Long)
1116
}

0 commit comments

Comments
 (0)