Skip to content

Commit

Permalink
Make namespaces opt-in (#2786)
Browse files Browse the repository at this point in the history
Closes #2594.

---------

Co-authored-by: StaNov <[email protected]>
Co-authored-by: Štěpán Granát <[email protected]>
  • Loading branch information
3 people authored Dec 20, 2024
1 parent 137d094 commit 4a8c938
Show file tree
Hide file tree
Showing 75 changed files with 874 additions and 282 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -82,24 +82,27 @@ class V2ImportController(
filteredFiles.map {
ImportFileDto(it.originalFilename ?: "", it.inputStream.readAllBytes())
}
val errors =
val (errors, warnings) =
importService.addFiles(
files = fileDtos,
project = projectHolder.projectEntity,
userAccount = authenticationFacade.authenticatedUserEntity,
params = params,
)
return getImportAddFilesResultModel(errors)
return getImportAddFilesResultModel(errors, warnings)
}

private fun getImportAddFilesResultModel(errors: List<ErrorResponseBody>): ImportAddFilesResultModel {
private fun getImportAddFilesResultModel(
errors: List<ErrorResponseBody>,
warnings: List<ErrorResponseBody>,
): ImportAddFilesResultModel {
val result: PagedModel<ImportLanguageModel>? =
try {
this.getImportResult(PageRequest.of(0, 100))
} catch (e: NotFoundException) {
null
}
return ImportAddFilesResultModel(errors, result)
return ImportAddFilesResultModel(errors, warnings, result)
}

@PutMapping("/apply")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.tolgee.activity.RequestActivity
import io.tolgee.activity.data.ActivityType
import io.tolgee.api.v2.controllers.IController
import io.tolgee.component.KeyComplexEditHelper
import io.tolgee.constants.Message
import io.tolgee.dtos.cacheable.LanguageDto
import io.tolgee.dtos.queryResults.KeyView
import io.tolgee.dtos.request.GetKeysRequestDto
Expand All @@ -16,6 +17,7 @@ import io.tolgee.dtos.request.key.DeleteKeysDto
import io.tolgee.dtos.request.key.EditKeyDto
import io.tolgee.dtos.request.translation.ImportKeysDto
import io.tolgee.dtos.request.translation.importKeysResolvable.ImportKeysResolvableDto
import io.tolgee.dtos.request.validators.exceptions.ValidationException
import io.tolgee.exceptions.NotFoundException
import io.tolgee.hateoas.key.KeyImportResolvableResultModel
import io.tolgee.hateoas.key.KeyModel
Expand Down Expand Up @@ -114,6 +116,7 @@ class KeyController(
checkTranslatePermission(dto)
checkCanStoreBigMeta(dto)
checkStateChangePermission(dto)
checkNamespaceFeature(dto.namespace)

val key = keyService.create(projectHolder.projectEntity, dto)
return ResponseEntity(keyWithDataModelAssembler.toModel(key), HttpStatus.CREATED)
Expand Down Expand Up @@ -162,6 +165,7 @@ class KeyController(
): KeyModel {
val key = keyService.findOptional(id).orElseThrow { NotFoundException() }
key.checkInProject()
checkNamespaceFeature(dto.namespace)
keyService.edit(id, dto)
val view = KeyView(key.id, key.name, key?.namespace?.name, key.keyMeta?.description, key.keyMeta?.custom)
return keyModelAssembler.toModel(view)
Expand Down Expand Up @@ -194,6 +198,7 @@ class KeyController(
@RequestBody @Valid
dto: ComplexEditKeyDto,
): KeyWithDataModel {
checkNamespaceFeature(dto.namespace)
return KeyComplexEditHelper(applicationContext, id, dto).doComplexUpdate()
}

Expand Down Expand Up @@ -382,4 +387,10 @@ class KeyController(
projectHolder.projectEntity.checkScreenshotsUploadPermission()
}
}

private fun checkNamespaceFeature(namespace: String?) {
if (!projectHolder.projectEntity.useNamespaces && namespace != null) {
throw ValidationException(Message.NAMESPACE_CANNOT_BE_USED_WHEN_FEATURE_IS_DISABLED)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ import org.springframework.hateoas.server.core.Relation
@Relation(collectionRelation = "fileIssues", itemRelation = "fileIssue")
open class ImportAddFilesResultModel(
val errors: List<ErrorResponseBody>,
val warnings: List<ErrorResponseBody>,
val result: PagedModel<ImportLanguageModel>?,
) : RepresentationModel<ImportAddFilesResultModel>()
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ open class ProjectModel(
val avatar: Avatar?,
val organizationOwner: SimpleOrganizationModel?,
val baseLanguage: LanguageModel?,
val useNamespaces: Boolean,
val defaultNamespace: NamespaceModel?,
val organizationRole: OrganizationRoleType?,
@Schema(description = "Current user's direct permission", example = "MANAGE")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class ProjectModelAssembler(
organizationRole = view.organizationRole,
organizationOwner = view.organizationOwner.let { simpleOrganizationModelAssembler.toModel(it) },
baseLanguage = baseLanguage.let { languageModelAssembler.toModel(LanguageDto.fromEntity(it, it.id)) },
useNamespaces = view.useNamespaces,
defaultNamespace = defaultNamespace,
directPermission = view.directPermission?.let { permissionModelAssembler.toModel(it) },
computedPermission = computedPermissionModelAssembler.toModel(computedPermissions),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class StartupImportCommandLineRunnerTest : AbstractSpringTest() {
assertThat(translationService.getAllByLanguageId(it.id)).hasSize(10)
}
assertThat(project.apiKeys.first().keyHash).isEqualTo("Zy98PdrKTEla1Ix7I1WbZPRoIDttk+Byk77tEjgRIzs=")
assertThat(project.useNamespaces).isFalse()
}
}

Expand All @@ -70,6 +71,7 @@ class StartupImportCommandLineRunnerTest : AbstractSpringTest() {
assertThat(projects).isNotEmpty
val project = projects.first()
project.namespaces.assert.hasSize(7)
assertThat(project.useNamespaces).isTrue()
}
}

Expand All @@ -80,6 +82,7 @@ class StartupImportCommandLineRunnerTest : AbstractSpringTest() {
assertThat(projects).isNotEmpty
val project = projects.first()
project.baseLanguage!!.tag.assert.isEqualTo("de")
assertThat(project.useNamespaces).isFalse()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ class SingleStepImportControllerTest : ProjectAuthControllerTest("/v2/projects/"
@ProjectJWTAuthTestMethod
fun `maps namespace`() {
saveAndPrepare()
enableNamespaces()
performImport(
projectId = testData.project.id,
listOf(Pair(jsonFileName, simpleJson)),
Expand All @@ -152,10 +153,35 @@ class SingleStepImportControllerTest : ProjectAuthControllerTest("/v2/projects/"
}
}

@Test
@ProjectJWTAuthTestMethod
fun `namespace mapping fails if namespaces are disabled`() {
saveAndPrepare()
performImport(
projectId = testData.project.id,
listOf(Pair(jsonFileName, simpleJson)),
getFileMappings(jsonFileName, namespace = "test"),
).andIsBadRequest.andHasErrorMessage(Message.NAMESPACE_CANNOT_BE_USED_WHEN_FEATURE_IS_DISABLED)
}

@Test
@ProjectJWTAuthTestMethod
fun `detected namespaces are ignored if namespaces are disabled`() {
saveAndPrepare()
performImport(
projectId = testData.project.id,
listOf(Pair("test-namespace/$jsonFileName", simpleJson)),
).andIsOk
executeInNewTransaction {
getTestTranslation(namespace = null).assert.isNotNull
}
}

@Test
@ProjectJWTAuthTestMethod
fun `maps null namespace from non-null mapping`() {
saveAndPrepare()
enableNamespaces()
val fileName = "guessed-ns/en.json"
performImport(
projectId = testData.project.id,
Expand All @@ -166,6 +192,13 @@ class SingleStepImportControllerTest : ProjectAuthControllerTest("/v2/projects/"
executeInNewTransaction {
getTestTranslation().assert.isNotNull
}

performImport(
projectId = testData.project.id,
listOf(Pair(fileName, simpleJson)),
getFileMappings(fileName, namespace = ""),
).andIsOk

performImport(
projectId = testData.project.id,
listOf(Pair(fileName, simpleJson)),
Expand Down Expand Up @@ -366,4 +399,10 @@ class SingleStepImportControllerTest : ProjectAuthControllerTest("/v2/projects/"
userAccount = testData.user
projectSupplier = { testData.project }
}

private fun enableNamespaces() {
val fetchedProject = projectService.find(testData.project.id)!!
fetchedProject.useNamespaces = true
projectService.save(fetchedProject)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.tolgee.testing.assert
import io.tolgee.testing.assertions.Assertions.assertThat
import io.tolgee.util.InMemoryFileStorage
import io.tolgee.util.performImport
import net.javacrumbs.jsonunit.core.internal.Node.JsonMap
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Value
Expand Down Expand Up @@ -241,14 +242,18 @@ class V2ImportControllerAddFilesTest : ProjectAuthControllerTest("/v2/projects/"
@Test
fun `pre-selects namespaces and languages correctly`() {
val base = dbPopulator.createBase()
base.project.useNamespaces = true
projectService.save(base.project)
commitTransaction()
tolgeeProperties.maxTranslationTextLength = 20

executeInNewTransaction {
performImport(
projectId = base.project.id,
listOf(Pair("namespaces.zip", namespacesZip)),
).andIsOk
).andIsOk.andAssertThatJson {
node("warnings").isArray.isEmpty()
}
}

executeInNewTransaction {
Expand All @@ -262,6 +267,27 @@ class V2ImportControllerAddFilesTest : ProjectAuthControllerTest("/v2/projects/"
}
}

@Test
fun `returns warning and blank namespaces when namespaces are detected but disabled`() {
val base = dbPopulator.createBase()
base.project.useNamespaces = false
projectService.save(base.project)
commitTransaction()

executeInNewTransaction {
performImport(
projectId = base.project.id,
listOf(Pair("namespaces.zip", namespacesZip)),
).andIsOk.andAssertThatJson {
node("warnings").isArray.hasSize(1)
node("warnings[0].code").isEqualTo("namespace_cannot_be_used_when_feature_is_disabled")
node("result._embedded.languages").isArray.allSatisfy {
(it as JsonMap)["namespace"].assert.isNull()
}
}
}
}

@Test
fun `works fine with Mac generated zip`() {
val base = dbPopulator.createBase()
Expand Down Expand Up @@ -322,6 +348,23 @@ class V2ImportControllerAddFilesTest : ProjectAuthControllerTest("/v2/projects/"
}
}

@Test
fun `import gets deleted after namespaces feature is toggled`() {
val base = dbPopulator.createBase()

performImport(projectId = base.project.id, listOf("simple.json" to simpleJson))
.andIsOk

assertThat(importService.getAllByProject(base.project.id)).isNotEmpty()

val project = projectService.get(base.project.id)
project.useNamespaces = !project.useNamespaces
projectService.save(project)
commitTransaction()

assertThat(importService.getAllByProject(project.id)).isEmpty()
}

private fun validateSavedJsonImportData(
project: Project,
userAccount: UserAccount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `creates key and namespace`() {
enableNamespaces()
performProjectAuthPost("keys", mapOf("name" to "super_key", "namespace" to "new_ns"))
.andIsCreated.andAssertThatJson {
node("name").isEqualTo("super_key")
Expand All @@ -50,6 +51,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `blank namespace doesn't create ns`() {
enableNamespaces()
performProjectAuthPost("keys", CreateKeyDto(name = "super_key", namespace = ""))
.andIsCreated
namespaceService.find("", project.id).assert.isNull()
Expand All @@ -58,6 +60,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `creates key in existing namespace`() {
enableNamespaces()
performProjectAuthPost("keys", CreateKeyDto(name = "super_key", namespace = "ns-1"))
.andIsCreated.andAssertThatJson {
node("name").isEqualTo("super_key")
Expand All @@ -69,6 +72,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `does not create key when not unique in ns`() {
enableNamespaces()
performProjectAuthPost("keys", CreateKeyDto(name = "key", "ns-1"))
.andAssertError
.isCustomValidation.hasMessage("key_exists")
Expand All @@ -77,6 +81,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `updates key in ns`() {
enableNamespaces()
performProjectAuthPut("keys/${testData.keyInNs1.id}", EditKeyDto(name = "super_k", "ns-2"))
.andIsOk.andAssertThatJson {
node("id").isValidId
Expand Down Expand Up @@ -109,6 +114,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `throws error when moving key to default ns where a key with same name already exists`() {
enableNamespaces()
val keyName = "super_ultra_cool_key"
val namespace = "super_ultra_cool_namespace"

Expand Down Expand Up @@ -155,6 +161,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `deletes ns when empty on update`() {
enableNamespaces()
performProjectAuthPut("keys/${testData.singleKeyInNs2.id}", EditKeyDto(name = "super_k", "ns-1"))
.andIsOk
namespaceService.find("ns-2", project.id).assert.isNull()
Expand All @@ -171,4 +178,57 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/

namespaceService.getAllInProject(testData.projectBuilder.self.id).assert.isEmpty()
}

@ProjectJWTAuthTestMethod
@Test
fun `key with namespace cannot be created when useNamespaces feature is disabled`() {
performProjectAuthPost("keys", mapOf("name" to "super_key", "namespace" to ""))
.andIsCreated.andAssertThatJson {
node("namespace").isNull()
}

performProjectAuthPost("keys", mapOf("name" to "super_key", "namespace" to "new_ns"))
.andIsBadRequest
.andAssertError
.isCustomValidation.hasMessage("namespace_cannot_be_used_when_feature_is_disabled")
}

@ProjectJWTAuthTestMethod
@Test
fun `key with namespace cannot be edited when useNamespaces feature is disabled`() {
performProjectAuthPut("keys/${testData.keyWithoutNs.id}", EditKeyDto(name = "super_k", ""))
.andIsOk.andAssertThatJson {
node("namespace").isNull()
}

performProjectAuthPut("keys/${testData.keyWithoutNs.id}", EditKeyDto(name = "super_k", "ns-2"))
.andIsBadRequest
.andAssertError
.isCustomValidation.hasMessage("namespace_cannot_be_used_when_feature_is_disabled")
}

@ProjectJWTAuthTestMethod
@Test
fun `key with namespace cannot be complex-edited when useNamespaces feature is disabled`() {
performProjectAuthPut(
"keys/${testData.keyWithoutNs.id}/complex-update",
mapOf("name" to "new-name", "namespace" to ""),
).andIsOk.andAssertThatJson {
node("namespace").isNull()
}

performProjectAuthPut(
"keys/${testData.keyWithoutNs.id}/complex-update",
mapOf("name" to "new-name", "namespace" to "ns-2"),
)
.andIsBadRequest
.andAssertError
.isCustomValidation.hasMessage("namespace_cannot_be_used_when_feature_is_disabled")
}

private fun enableNamespaces() {
val projectFetched = projectService.get(project.id)
projectFetched.useNamespaces = true
projectService.save(projectFetched)
}
}
Loading

0 comments on commit 4a8c938

Please sign in to comment.