diff --git a/service/tools/maven-plugin/README.adoc b/service/tools/maven-plugin/README.adoc index c653b239065..721632db709 100644 --- a/service/tools/maven-plugin/README.adoc +++ b/service/tools/maven-plugin/README.adoc @@ -21,11 +21,11 @@ The plugin uses a goal prefix of `timefold` (see the plugin configuration in the - Fails the build when no `ai.timefold.solver.enterprise` artifact is on the resolved classpath, i.e. the model was built with the Community Edition, which Timefold Platform does not accept. Both checks only run when `timefold:deploy` is among the requested goals, run even when `timefold.model.configuration.skip` is set. - Fetches platform identity/config by calling GET /api/platform/v1/aboutme?includeConfig=true - Requires a platform personal access token available via environment variable `TIMEFOLD_PAT` (see "Authentication" below) -- If a single accountId is returned by the platform and `timefold.accountId` wasn't provided, the plugin will use it -- Fails the build when the account id can neither be taken from `timefold.accountId` nor derived from the platform response, i.e. when the personal access token is associated with no account or with several of them. In the latter case `timefold.accountId` has to be set explicitly +- If a single namespace is returned by the platform and `timefold.namespace` wasn't provided, the plugin will use it. The platform reports the namespaces either as `namespaces` or, before the migration to that name is complete, as `accountIds`; both are accepted +- Fails the build when the namespace can neither be taken from `timefold.namespace` nor derived from the platform response, i.e. when the personal access token is associated with no namespace or with several of them. In the latter case `timefold.namespace` has to be set explicitly - Writes a properties file at `target/generated-resources/timefold-build.properties` with entries such as: ** `quarkus.container-image.registry` — value taken from platform config.containerRegistry -** `quarkus.container-image.group` — the account id used +** `quarkus.container-image.group` — the namespace used ** `quarkus.container-image.push` — set to `true` ** `image.native-suffix` — set to `""` when native support is disabled (default) @@ -63,7 +63,7 @@ These are the most important configuration properties for the plugin. They are s === Configure goal specific -- `timefold.accountId` (String) — optional account id to use; if not provided and platform returns a single account, the plugin uses it. Set via `` or `-Dtimefold.accountId=...`. +- `timefold.namespace` (String) — optional namespace to use; if not provided and platform returns a single namespace, the plugin uses it. Set via `` or `-Dtimefold.namespace=...`. - `timefold.model.configuration.skip` (boolean, default=false) — skip `timefold:configure`. Property name remains `timefold.model.configuration.skip`. - `timefold.model.nativeSupported` (boolean, default=false) — whether target image should use native suffix; when `false` `image.native-suffix` is set to an empty string allowing JVM builds to be used for native image use cases. Use `-Dtimefold.model.nativeSupported=true` to mark native support. @@ -123,7 +123,7 @@ Bind `timefold:configure` to the `initialize` phase via `` so it run UUID_OF_THE_TENANT - + diff --git a/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/ConfigureMojo.java b/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/ConfigureMojo.java index a9cb292ee79..65d57c13482 100644 --- a/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/ConfigureMojo.java +++ b/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/ConfigureMojo.java @@ -26,7 +26,7 @@ @Mojo(name = "configure", defaultPhase = LifecyclePhase.INITIALIZE, requiresDependencyResolution = ResolutionScope.COMPILE) public class ConfigureMojo extends AbstractPlatformModelMojo { - protected static final String PROP_ACCOUNT_ID = "timefold.accountId"; + protected static final String PROP_NAMESPACE = "timefold.namespace"; protected static final String PROP_MODEL_NATIVE_SUPPORTED = "timefold.model.nativeSupported"; @@ -46,10 +46,10 @@ public class ConfigureMojo extends AbstractPlatformModelMojo { private MavenProject project; /** - * Account id that model is associated with + * Namespace that model is associated with */ - @Parameter(property = PROP_ACCOUNT_ID, required = false) - protected String accountId; + @Parameter(property = PROP_NAMESPACE, required = false) + protected String namespace; /** * Determines if the native build of the model is supported and by that should be defined in model descriptor @@ -87,13 +87,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { PlatformIdentityInfo info = fetchPlatformIdentityInfo(true); if (info == null || !info.hasPushAccessRights()) { - throw new RuntimeException("No access to deploy model on Timefold Platform"); + throw new MojoFailureException("No access to deploy model on Timefold Platform"); } - var resolvedAccountId = resolveAccountId(info); + var resolvedNamespace = resolveNamespace(info); - if (!info.hasAccessToAccountId(resolvedAccountId)) { - throw new RuntimeException( - "No access to configured account id " + resolvedAccountId + " or account not configured"); + if (!info.hasAccessToNamespace(resolvedNamespace)) { + // Only a namespace configured explicitly can get here; one derived from the token is always allowed. + throw new MojoFailureException(describeMissingNamespaceAccess(info, resolvedNamespace)); } Path path = Paths.get("target", "generated-resources", "timefold-build.properties"); @@ -109,7 +109,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { timefoldBuildProperties.setProperty("quarkus.profile", "container"); timefoldBuildProperties.setProperty("quarkus.container-image.build", "true"); timefoldBuildProperties.setProperty("quarkus.container-image.registry", registry); - timefoldBuildProperties.setProperty("quarkus.container-image.group", resolvedAccountId); + timefoldBuildProperties.setProperty("quarkus.container-image.group", resolvedNamespace); // configure container image and arguments based on model parent pom settings timefoldBuildProperties.setProperty("quarkus.jib.jvm-additional-arguments", @@ -146,42 +146,55 @@ public void execute() throws MojoExecutionException, MojoFailureException { } /** - * Resolves the account id the model is deployed under, which becomes the group of the container image. It is either - * configured explicitly, or, when the personal access token is associated with exactly one account, that account. + * Resolves the namespace the model is deployed under, which becomes the group of the container image. It is either + * configured explicitly, or, when the personal access token is associated with exactly one namespace, that namespace. * - * @throws MojoFailureException when the account id is neither configured nor unambiguously derivable from the + * @throws MojoFailureException when the namespace is neither configured nor unambiguously derivable from the * personal access token; without it the container image cannot be named, so the build must not continue. */ - protected String resolveAccountId(PlatformIdentityInfo info) throws MojoFailureException { - String configuredAccountId = getPropertyOrParameter(PROP_ACCOUNT_ID, this.accountId); - if (configuredAccountId != null && !configuredAccountId.isBlank()) { - return configuredAccountId.trim(); + protected String resolveNamespace(PlatformIdentityInfo info) throws MojoFailureException { + String configuredNamespace = getPropertyOrParameter(PROP_NAMESPACE, this.namespace); + if (configuredNamespace != null && !configuredNamespace.isBlank()) { + return configuredNamespace.trim(); } - Set accountIds = info.accountIds() == null ? Set.of() : info.accountIds(); - if (accountIds.size() == 1) { - return accountIds.iterator().next(); + Set namespaces = info.namespaces(); + if (namespaces.size() == 1) { + return namespaces.iterator().next(); } - if (accountIds.isEmpty()) { + if (namespaces.isEmpty()) { throw new MojoFailureException(""" - Unable to resolve the Timefold Platform account id: the personal access token is not associated with \ - any account, so the container image of this model cannot be built. - Use a personal access token of an account that is allowed to deploy models. + Unable to resolve the Timefold Platform namespace: the personal access token is not associated with \ + any namespace, so the container image of this model cannot be built. + Use a personal access token that is associated with a namespace allowed to deploy models. See https://docs.timefold.ai/timefold-solver/latest/deploying-to-platform/guide"""); } throw new MojoFailureException(""" - Unable to resolve the Timefold Platform account id: the personal access token is associated with %d \ - accounts (%s), so the account to deploy this model to has to be configured explicitly. + Unable to resolve the Timefold Platform namespace: the personal access token is associated with %d \ + namespaces (%s), so the namespace to deploy this model to has to be configured explicitly. Either pass it on the command line: - mvn clean package -D%s= timefold:deploy + mvn clean package -D%s= timefold:deploy or declare it in the plugin configuration: - ... + ... See https://docs.timefold.ai/timefold-solver/latest/deploying-to-platform/guide""" - .formatted(accountIds.size(), accountIds.stream().sorted().collect(Collectors.joining(", ")), - PROP_ACCOUNT_ID)); + .formatted(namespaces.size(), namespaces.stream().sorted().collect(Collectors.joining(", ")), + PROP_NAMESPACE)); + } + + /** + * Explains why the configured namespace cannot be deployed to, telling a namespace the token does not grant apart + * from a token that grants no namespace at all, as those need different fixes. + */ + private static String describeMissingNamespaceAccess(PlatformIdentityInfo info, String configuredNamespace) { + if (info.namespaces().isEmpty()) { + return "The personal access token is not associated with any namespace, so this model cannot be deployed " + + "to the configured namespace " + configuredNamespace; + } + return "The personal access token is not associated with the configured namespace %s, but with %s" + .formatted(configuredNamespace, info.namespaces().stream().sorted().collect(Collectors.joining(", "))); } /** diff --git a/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/PermissionsMojo.java b/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/PermissionsMojo.java index 9abd13ba6dc..afbc599ff2b 100644 --- a/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/PermissionsMojo.java +++ b/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/PermissionsMojo.java @@ -32,7 +32,7 @@ private void report(PlatformIdentityInfo info) { getLog().info(" User : " + orNone(info.user())); getLog().info(" Scopes : " + joinStrings(info.scopes())); getLog().info(" Tenants : " + joinUuids(info.tenants())); - getLog().info(" Namespaces : " + joinStrings(info.accountIds())); + getLog().info(" Namespaces : " + joinStrings(info.namespaces())); var selectedTenants = getTenants(); if (selectedTenants != null && !selectedTenants.isEmpty()) { diff --git a/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/client/PlatformIdentityInfo.java b/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/client/PlatformIdentityInfo.java index 5d7362e62d5..f130b6e1e76 100644 --- a/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/client/PlatformIdentityInfo.java +++ b/service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/client/PlatformIdentityInfo.java @@ -3,24 +3,34 @@ import java.util.Set; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** + * The identity behind the personal access token, as reported by the platform's {@code aboutme} endpoint. The namespaces + * the token is associated with are being migrated by Timefold Platform from {@code accountIds} to {@code namespaces}, + * so both field names are accepted and either one may be missing from the response. + */ +@JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(value = Include.NON_NULL) -public record PlatformIdentityInfo(String user, Set scopes, Set accountIds, Set tenants, - ConfigurationInfo config) { +public record PlatformIdentityInfo(String user, Set scopes, @JsonAlias("accountIds") Set namespaces, + Set tenants, ConfigurationInfo config) { private static final Set REQUIRED_SCOPES = Set.of("registered-model:create", "registered-model:update"); - public boolean hasPushAccessRights() { - return scopes().stream().anyMatch(scope -> REQUIRED_SCOPES.contains(scope)); + public PlatformIdentityInfo { + // The platform may leave these out of the response, so normalize them and keep the rest of the plugin null free. + scopes = scopes == null ? Set.of() : scopes; + namespaces = namespaces == null ? Set.of() : namespaces; } - public boolean hasAccessToAccountId(String account) { - if (accountIds == null || accountIds.isEmpty()) { - return false; - } + public boolean hasPushAccessRights() { + return scopes().stream().anyMatch(REQUIRED_SCOPES::contains); + } - return accountIds().contains(account); + public boolean hasAccessToNamespace(String namespace) { + return namespaces().contains(namespace); } } diff --git a/service/tools/maven-plugin/src/test/java/ai/timefold/solver/tools/maven/ConfigureMojoTest.java b/service/tools/maven-plugin/src/test/java/ai/timefold/solver/tools/maven/ConfigureMojoTest.java index 2e5a20f6abb..5ab282a36b5 100644 --- a/service/tools/maven-plugin/src/test/java/ai/timefold/solver/tools/maven/ConfigureMojoTest.java +++ b/service/tools/maven-plugin/src/test/java/ai/timefold/solver/tools/maven/ConfigureMojoTest.java @@ -51,7 +51,7 @@ public class ConfigureMojoTest { private MavenSession session; @BeforeEach - void setUp() throws IOException { + void setUp() { log.clear(); wm1.resetAll(); @@ -73,6 +73,25 @@ void setUp() throws IOException { } """))); + // the platform reports the migrated field name instead of accountIds, next to a field the plugin does not know + wm1.stubFor(get(urlPathEqualTo("/api/platform/v1/aboutme")) + .withHeader("Authorization", equalTo("Bearer namespaces")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + { + "user" : "test@email.com", + "scopes" : ["registered-model:create"], + "tenants" : [], + "namespaces" : ["test"], + "roles" : ["model-publisher"], + "config" : { + "containerRegistry" : "test.registry.com" + } + } + """))); + wm1.stubFor(get(urlPathEqualTo("/api/platform/v1/aboutme")) .withHeader("Authorization", equalTo("Bearer wrong")) .atPriority(10) @@ -97,9 +116,9 @@ void setUp() throws IOException { } """))); - // the token is allowed to deploy models, but is not associated with any account + // the token is allowed to deploy models, but is not associated with any namespace wm1.stubFor(get(urlPathEqualTo("/api/platform/v1/aboutme")) - .withHeader("Authorization", equalTo("Bearer noaccounts")) + .withHeader("Authorization", equalTo("Bearer emptyaccountids")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") @@ -115,9 +134,9 @@ void setUp() throws IOException { } """))); - // the platform does not report the accountIds field at all + // the platform reports neither the accountIds nor the namespaces field wm1.stubFor(get(urlPathEqualTo("/api/platform/v1/aboutme")) - .withHeader("Authorization", equalTo("Bearer noaccountids")) + .withHeader("Authorization", equalTo("Bearer nonamespaces")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") @@ -132,7 +151,7 @@ void setUp() throws IOException { } """))); - // the token is associated with several accounts, so the account id cannot be derived from it + // the token is associated with several namespaces, so the namespace cannot be derived from it wm1.stubFor(get(urlPathEqualTo("/api/platform/v1/aboutme")) .withHeader("Authorization", equalTo("Bearer multipleaccounts")) .willReturn(aResponse() @@ -179,23 +198,33 @@ public void testConfigureSuccessfully(ConfigureMojo mojo) throws Exception { // assert that plugin executed and produced expected logs log.assertContains("Configured Timefold Platform integration", Level.INFO); - Path buildProperties = Paths.get("target", "generated-resources", "timefold-build.properties"); - - // assert the build properties file exists - assertThat(Files.exists(buildProperties)).isTrue(); - // load configured build properties and assert expected entry - Properties props = new Properties(); - try (InputStream in = Files.newInputStream(buildProperties)) { - props.load(in); - } - assertThat(props) - .containsEntry("quarkus.container-image.group", "test")// test is returned from aboutme endpoint as this is the account that access token grants + assertThat(readBuildProperties()) + // test is returned from aboutme endpoint as this is the namespace that access token grants + .containsEntry("quarkus.container-image.group", "test") .containsEntry("quarkus.container-image.registry", "test.registry.com") .containsEntry("quarkus.container-image.push", "true") .containsEntry("image.native-suffix", ""); } + @Test + @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") + public void testConfigureSuccessfullyWhenPlatformReportsNamespaces(ConfigureMojo mojo) throws Exception { + + session.getRequest().setGoals(List.of("timefold:deploy")); + setEnterpriseModel(mojo); + + mojo.setAccessTokenProvider(new TestAccessTokenProvider("namespaces")); + mojo.setLog(log); + mojo.platformUrl = wm1.getRuntimeInfo().getHttpBaseUrl(); + mojo.execute(); + + log.assertContains("Configured Timefold Platform integration", Level.INFO); + + // the namespace is resolved from the migrated field name exactly like it is from accountIds + assertThat(readBuildProperties()).containsEntry("quarkus.container-image.group", "test"); + } + @Test @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") public void testConfigureSuccessfullyWithTrailingSlashInPlatformUrl(ConfigureMojo mojo) throws Exception { @@ -216,7 +245,7 @@ public void testConfigureSuccessfullyWithTrailingSlashInPlatformUrl(ConfigureMoj @Test @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") - public void testConfigureFailsWithBlankPlatformUrl(ConfigureMojo mojo) throws Exception { + public void testConfigureFailsWithBlankPlatformUrl(ConfigureMojo mojo) { session.getRequest().setGoals(List.of("timefold:deploy")); setEnterpriseModel(mojo); @@ -249,18 +278,10 @@ public void testConfigureSuccessfullyNativeSupported(ConfigureMojo mojo) throws // assert that plugin executed and produced expected logs log.assertContains("Configured Timefold Platform integration", Level.INFO); - Path buildProperties = Paths.get("target", "generated-resources", "timefold-build.properties"); - - // assert the build properties file exists - assertThat(Files.exists(buildProperties)).isTrue(); - // load configured build properties and assert expected entry - Properties props = new Properties(); - try (InputStream in = Files.newInputStream(buildProperties)) { - props.load(in); - } - assertThat(props) - .containsEntry("quarkus.container-image.group", "test")// test is returned from aboutme endpoint as this is the account that access token grants + assertThat(readBuildProperties()) + // test is returned from aboutme endpoint as this is the namespace that access token grants + .containsEntry("quarkus.container-image.group", "test") .containsEntry("quarkus.container-image.registry", "test.registry.com") .containsEntry("quarkus.container-image.push", "true") .doesNotContainKey("image.native-suffix"); @@ -268,7 +289,7 @@ public void testConfigureSuccessfullyNativeSupported(ConfigureMojo mojo) throws @Test @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") - public void testConfigureNotAuthorizaed(ConfigureMojo mojo) throws Exception { + public void testConfigureNotAuthorized(ConfigureMojo mojo) { session.getRequest().setGoals(List.of("timefold:deploy")); setEnterpriseModel(mojo); @@ -285,7 +306,7 @@ public void testConfigureNotAuthorizaed(ConfigureMojo mojo) throws Exception { @Test @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") - public void testConfigureMissingAccessToken(ConfigureMojo mojo) throws Exception { + public void testConfigureMissingAccessToken(ConfigureMojo mojo) { session.getRequest().setGoals(List.of("timefold:deploy")); setEnterpriseModel(mojo); @@ -302,9 +323,9 @@ public void testConfigureMissingAccessToken(ConfigureMojo mojo) throws Exception } @Test - @MojoParameter(name = "accountId", value = "company") + @MojoParameter(name = "namespace", value = "company") @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") - public void testConfigureNotAuthorizaedForAccountId(ConfigureMojo mojo) throws Exception { + public void testConfigureNotAuthorizedForNamespace(ConfigureMojo mojo) { session.getRequest().setGoals(List.of("timefold:deploy")); setEnterpriseModel(mojo); @@ -313,50 +334,71 @@ public void testConfigureNotAuthorizaedForAccountId(ConfigureMojo mojo) throws E mojo.setLog(log); mojo.platformUrl = wm1.getRuntimeInfo().getHttpBaseUrl(); - assertThatThrownBy(mojo::execute).isInstanceOf(RuntimeException.class) - .hasMessage("No access to configured account id company or account not configured"); + assertThatThrownBy(mojo::execute).isInstanceOf(MojoFailureException.class) + .hasMessage("The personal access token is not associated with the configured namespace company, " + + "but with test"); + + wm1.verify(1, getRequestedFor(urlPathEqualTo("/api/platform/v1/aboutme"))); + } + + @Test + @MojoParameter(name = "namespace", value = "company") + @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") + public void testConfigureNotAuthorizedForNamespaceWhenTokenHasNoNamespace(ConfigureMojo mojo) { + + session.getRequest().setGoals(List.of("timefold:deploy")); + setEnterpriseModel(mojo); + + mojo.setAccessTokenProvider(new TestAccessTokenProvider("emptyaccountids")); + mojo.setLog(log); + mojo.platformUrl = wm1.getRuntimeInfo().getHttpBaseUrl(); + + // the configured namespace short circuits the resolution, so the token granting none is only reported here + assertThatThrownBy(mojo::execute).isInstanceOf(MojoFailureException.class) + .hasMessage("The personal access token is not associated with any namespace, " + + "so this model cannot be deployed to the configured namespace company"); wm1.verify(1, getRequestedFor(urlPathEqualTo("/api/platform/v1/aboutme"))); } @Test @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") - public void testConfigureFailsWhenTokenHasNoAccount(ConfigureMojo mojo) { + public void testConfigureFailsWhenTokenHasNoNamespace(ConfigureMojo mojo) { session.getRequest().setGoals(List.of("timefold:deploy")); setEnterpriseModel(mojo); - mojo.setAccessTokenProvider(new TestAccessTokenProvider("noaccounts")); + mojo.setAccessTokenProvider(new TestAccessTokenProvider("emptyaccountids")); mojo.setLog(log); mojo.platformUrl = wm1.getRuntimeInfo().getHttpBaseUrl(); assertThatThrownBy(mojo::execute).isInstanceOf(MojoFailureException.class) - .hasMessageContaining("Unable to resolve the Timefold Platform account id") - .hasMessageContaining("not associated with any account"); + .hasMessageContaining("Unable to resolve the Timefold Platform namespace") + .hasMessageContaining("not associated with any namespace"); wm1.verify(1, getRequestedFor(urlPathEqualTo("/api/platform/v1/aboutme"))); } @Test @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") - public void testConfigureFailsWhenPlatformReportsNoAccountIds(ConfigureMojo mojo) { + public void testConfigureFailsWhenPlatformReportsNoNamespaceField(ConfigureMojo mojo) { session.getRequest().setGoals(List.of("timefold:deploy")); setEnterpriseModel(mojo); - mojo.setAccessTokenProvider(new TestAccessTokenProvider("noaccountids")); + mojo.setAccessTokenProvider(new TestAccessTokenProvider("nonamespaces")); mojo.setLog(log); mojo.platformUrl = wm1.getRuntimeInfo().getHttpBaseUrl(); assertThatThrownBy(mojo::execute).isInstanceOf(MojoFailureException.class) - .hasMessageContaining("not associated with any account"); + .hasMessageContaining("not associated with any namespace"); wm1.verify(1, getRequestedFor(urlPathEqualTo("/api/platform/v1/aboutme"))); } @Test @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") - public void testConfigureFailsWhenAccountIdIsAmbiguous(ConfigureMojo mojo) { + public void testConfigureFailsWhenNamespaceIsAmbiguous(ConfigureMojo mojo) { session.getRequest().setGoals(List.of("timefold:deploy")); setEnterpriseModel(mojo); @@ -366,16 +408,16 @@ public void testConfigureFailsWhenAccountIdIsAmbiguous(ConfigureMojo mojo) { mojo.platformUrl = wm1.getRuntimeInfo().getHttpBaseUrl(); assertThatThrownBy(mojo::execute).isInstanceOf(MojoFailureException.class) - .hasMessageContaining("associated with 2 accounts (company, test)") - .hasMessageContaining("-Dtimefold.accountId="); + .hasMessageContaining("associated with 2 namespaces (company, test)") + .hasMessageContaining("-Dtimefold.namespace="); wm1.verify(1, getRequestedFor(urlPathEqualTo("/api/platform/v1/aboutme"))); } @Test - @MojoParameter(name = "accountId", value = "company") + @MojoParameter(name = "namespace", value = "company") @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") - public void testConfigureUsesConfiguredAccountIdWhenSeveralAreAvailable(ConfigureMojo mojo) throws Exception { + public void testConfigureUsesConfiguredNamespaceWhenSeveralAreAvailable(ConfigureMojo mojo) throws Exception { session.getRequest().setGoals(List.of("timefold:deploy")); setEnterpriseModel(mojo); @@ -389,18 +431,12 @@ public void testConfigureUsesConfiguredAccountIdWhenSeveralAreAvailable(Configur log.assertContains("Configured Timefold Platform integration", Level.INFO); - Path buildProperties = Paths.get("target", "generated-resources", "timefold-build.properties"); - - Properties props = new Properties(); - try (InputStream in = Files.newInputStream(buildProperties)) { - props.load(in); - } - assertThat(props).containsEntry("quarkus.container-image.group", "company"); + assertThat(readBuildProperties()).containsEntry("quarkus.container-image.group", "company"); } @Test @InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml") - public void testConfigureWrongScopes(ConfigureMojo mojo) throws Exception { + public void testConfigureWrongScopes(ConfigureMojo mojo) { session.getRequest().setGoals(List.of("timefold:deploy")); setEnterpriseModel(mojo); @@ -409,7 +445,7 @@ public void testConfigureWrongScopes(ConfigureMojo mojo) throws Exception { mojo.setLog(log); mojo.platformUrl = wm1.getRuntimeInfo().getHttpBaseUrl(); - assertThatThrownBy(mojo::execute).isInstanceOf(RuntimeException.class) + assertThatThrownBy(mojo::execute).isInstanceOf(MojoFailureException.class) .hasMessage("No access to deploy model on Timefold Platform"); wm1.verify(1, getRequestedFor(urlPathEqualTo("/api/platform/v1/aboutme"))); @@ -537,6 +573,17 @@ public void testConfigureSkipsEnterpriseCheckWhenDeployNotRequested(ConfigureMoj wm1.verify(0, getRequestedFor(urlPathEqualTo("/api/platform/v1/aboutme"))); } + private static Properties readBuildProperties() throws IOException { + Path buildProperties = Paths.get("target", "generated-resources", "timefold-build.properties"); + assertThat(Files.exists(buildProperties)).isTrue(); + + Properties props = new Properties(); + try (InputStream in = Files.newInputStream(buildProperties)) { + props.load(in); + } + return props; + } + /** * Mimics a correctly set up platform model: it inherits from {@code timefold-solver-service-parent} and was built * with the {@code enterprise} profile active.