diff --git a/bubble-server/src/main/java/bubble/ApiConstants.java b/bubble-server/src/main/java/bubble/ApiConstants.java index 219b6fde..2dd17322 100644 --- a/bubble-server/src/main/java/bubble/ApiConstants.java +++ b/bubble-server/src/main/java/bubble/ApiConstants.java @@ -278,6 +278,7 @@ public class ApiConstants { public static String getReferer(ContainerRequest ctx) { return ctx.getHeaderString(REFERER); } public static final String DETECT_LOCALE = "detect"; + public static final String DETECT_TIMEZONE = "detect"; public static List getLocales(ContainerRequest ctx, String defaultLocale) { final List locales = new ArrayList<>(); diff --git a/bubble-server/src/main/java/bubble/dao/bill/AccountPaymentMethodDAO.java b/bubble-server/src/main/java/bubble/dao/bill/AccountPaymentMethodDAO.java index d9257e78..29fa0784 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/AccountPaymentMethodDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/AccountPaymentMethodDAO.java @@ -21,6 +21,7 @@ import java.util.List; import static org.cobbzilla.util.daemon.ZillaRuntime.now; import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; +import static org.hibernate.criterion.Restrictions.*; @Repository @Slf4j public class AccountPaymentMethodDAO extends AccountOwnedEntityDAO { @@ -42,6 +43,14 @@ public class AccountPaymentMethodDAO extends AccountOwnedEntityDAO findByAccountAndNotPromoAndNotDeleted(String account) { + return list(criteria().add(and( + eq("account", account), + ne("paymentMethodType", PaymentMethodType.promotional_credit), + isNull("deleted") + ))); + } + public List findByAccountAndCloud(String accountUuid, String cloud) { return findByFields("account", accountUuid, "cloud", cloud); } diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java b/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java index 17f084b1..2fd36711 100644 --- a/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java @@ -163,16 +163,16 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { public boolean hasForkHost () { return !empty(forkHost); } @Transient @Getter @Setter private transient Boolean syncPassword = null; - public boolean syncPassword () { return bool(syncPassword); } + public boolean syncPassword () { return syncPassword == null || syncPassword; } @Transient @Getter @Setter private Boolean launchLock; public boolean launchLock() { return bool(launchLock); } @Transient @Getter @Setter private transient Boolean sendErrors = null; - public boolean sendErrors () { return bool(sendErrors); } + public boolean sendErrors () { return sendErrors == null || sendErrors; } @Transient @Getter @Setter private transient Boolean sendMetrics = null; - public boolean sendMetrics () { return bool(sendMetrics); } + public boolean sendMetrics () { return sendMetrics == null || sendMetrics; } public BubbleNetwork bubbleNetwork(Account account, BubbleDomain domain, diff --git a/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java b/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java index 0dd8ccf2..bae25fc1 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java @@ -151,13 +151,14 @@ public class AccountOwnedResource sshKeys = sshKeyDAO.findByAccount(caller.getUuid()); + if (empty(sshKeys)) { + request.setSshKey(null); + } else { + request.setSshKey(sshKeys.get(0).getUuid()); + } } else { request.setSshKey(null); // if it's an empty string, make it null (see simple_network test) } - final BubbleDomain domain = domainDAO.findByAccountAndId(caller.getUuid(), request.getDomain()); + BubbleDomain domain = domainDAO.findByAccountAndId(caller.getUuid(), request.getDomain()); if (domain == null) { - log.info("setReferences: domain not found: "+request.getDomain()+" for caller: "+caller.getUuid()); - errors.addViolation("err.domain.required"); + final List domains = domainDAO.findByAccount(caller.getUuid()); + if (empty(domains)) return die("setReferences: no domains found for account: "+caller.getUuid()); + domain = domains.get(0); + request.setDomain(domain.getUuid()); } else { request.setDomain(domain.getUuid()); - final BubbleNetwork existingNetwork = networkDAO.findByNameAndDomainName(request.getName(), domain.getName()); if (existingNetwork != null) errors.addViolation("err.name.networkNameAlreadyExists"); } @@ -138,7 +153,15 @@ public class AccountPlansResource extends AccountOwnedResource forkHost.equals("." + d.getName())) + .findFirst().orElse(null); + if (foundDomain == null) { + errors.addViolation("err.forkHost.domain.notFound"); + } else { + request.setDomain(foundDomain.getUuid()); + } + } else if (domain != null) { request.setName(domain.networkFromFqdn(forkHost, errors)); validateName(request, errors); @@ -146,11 +169,8 @@ public class AccountPlansResource extends AccountOwnedResource NAME_MAXLEN) { errors.addViolation("err.name.tooLong"); @@ -160,9 +180,14 @@ public class AccountPlansResource extends AccountOwnedResource paymentMethods = paymentMethodDAO.findByAccountAndNotPromoAndNotDeleted(caller.getUuid()); + if (empty(paymentMethods)) { + errors.addViolation("err.paymentMethod.required"); } else { - paymentMethod = request.getPaymentMethodObject(); + request.setPaymentMethodObject(paymentMethods.get(0)); } - if (paymentMethod != null && plan != null) { - if (paymentMethod.hasPromotion() || paymentMethod.getPaymentMethodType() == PaymentMethodType.promotional_credit) { - // cannot pay with a promo credit, must supply another payment method. - // promos will be applied at purchase, and may result in no charge to this payment method - errors.addViolation("err.purchase.paymentMethodNotFound"); - } else { - paymentMethod.setAccount(caller.getUuid()).validate(errors, configuration); - } + } + if (request.getPaymentMethodObject().hasUuid()) { + paymentMethod = paymentMethodDAO.findByUuid(request.getPaymentMethodObject().getUuid()); + if (paymentMethod == null) errors.addViolation("err.purchase.paymentMethodNotFound"); + } else { + paymentMethod = request.getPaymentMethodObject(); + } + if (paymentMethod != null && plan != null) { + if (paymentMethod.hasPromotion() || paymentMethod.getPaymentMethodType() == PaymentMethodType.promotional_credit) { + // cannot pay with a promo credit, must supply another payment method. + // promos will be applied at purchase, and may result in no charge to this payment method + errors.addViolation("err.purchase.paymentMethodNotFound"); + } else { + paymentMethod.setAccount(caller.getUuid()).validate(errors, configuration); } } } diff --git a/bubble-server/src/main/java/bubble/service/cloud/NodeLauncher.java b/bubble-server/src/main/java/bubble/service/cloud/NodeLauncher.java index 30a5b7eb..c013a527 100644 --- a/bubble-server/src/main/java/bubble/service/cloud/NodeLauncher.java +++ b/bubble-server/src/main/java/bubble/service/cloud/NodeLauncher.java @@ -7,6 +7,7 @@ package bubble.service.cloud; import bubble.model.cloud.BubbleNetwork; import bubble.model.cloud.BubbleNode; import bubble.notify.NewNodeNotification; +import bubble.server.BubbleConfiguration; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,6 +26,7 @@ public class NodeLauncher implements Runnable { private final AtomicReference lock; private final StandardNetworkService networkService; private final NodeLaunchMonitor launchMonitor; + private final BubbleConfiguration configuration; @Override public void run() { final String networkUuid = newNodeRequest.getNetwork(); @@ -98,7 +100,7 @@ public class NodeLauncher implements Runnable { die("NodeLauncher.run: unknown launch exception (type="+launchException.getType()+"): "+shortError(launchException), launchException); } } else { - die("NodeLauncher.run: fatal launch exception: " + shortError(exception), exception); + if (!configuration.testMode()) die("NodeLauncher.run: fatal launch exception: " + shortError(exception), exception); } } if (node != null && node.isRunning()) { diff --git a/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java b/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java index 585bc5b2..b5513902 100644 --- a/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java +++ b/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java @@ -817,7 +817,7 @@ public class StandardNetworkService implements NetworkService { public void backgroundNewNode(NewNodeNotification newNodeRequest, final String existingLock) { final AtomicReference lock = new AtomicReference<>(existingLock); - daemon(new NodeLauncher(newNodeRequest, lock, this, launchMonitor)); + daemon(new NodeLauncher(newNodeRequest, lock, this, launchMonitor, configuration)); } public boolean stopNetwork(final BubbleNetwork network) { diff --git a/bubble-server/src/main/resources/messages b/bubble-server/src/main/resources/messages index 35f36a84..7ddb8f71 160000 --- a/bubble-server/src/main/resources/messages +++ b/bubble-server/src/main/resources/messages @@ -1 +1 @@ -Subproject commit 35f36a84d84cb1f73dd8cc1f18e044657675b7f9 +Subproject commit 7ddb8f71fc43a5b5ce1056944e1f1876ee586f14 diff --git a/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java b/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java index d29583f0..b15b1dc8 100644 --- a/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java +++ b/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java @@ -67,6 +67,12 @@ public abstract class ActivatedBubbleModelTestBase extends BubbleModelTestBase { super.beforeStart(server); } + public void mockNetwork(RestServer server) { + final BubbleConfiguration configuration = server.getConfiguration(); + configuration.setSpringContextPath("classpath:/spring-mock-network.xml"); + configuration.getStaticAssets().setLocalOverride(null); + } + public String getDefaultDomain() { return "example.com"; } @Override protected String[] getSqlPostScripts() { return hasExistingDb ? null : super.getSqlPostScripts(); } diff --git a/bubble-server/src/test/java/bubble/test/payment/PaymentTestBase.java b/bubble-server/src/test/java/bubble/test/payment/PaymentTestBase.java index b01b4759..dbb6081a 100644 --- a/bubble-server/src/test/java/bubble/test/payment/PaymentTestBase.java +++ b/bubble-server/src/test/java/bubble/test/payment/PaymentTestBase.java @@ -15,9 +15,7 @@ public class PaymentTestBase extends ActivatedBubbleModelTestBase { @Override protected String getManifest() { return "manifest-test"; } @Override public void beforeStart(RestServer server) { - final BubbleConfiguration configuration = server.getConfiguration(); - configuration.setSpringContextPath("classpath:/spring-mock-network.xml"); - configuration.getStaticAssets().setLocalOverride(null); + mockNetwork(server); super.beforeStart(server); } diff --git a/bubble-server/src/test/java/bubble/test/system/NetworkTest.java b/bubble-server/src/test/java/bubble/test/system/NetworkTest.java index f64aca39..4c95574f 100644 --- a/bubble-server/src/test/java/bubble/test/system/NetworkTest.java +++ b/bubble-server/src/test/java/bubble/test/system/NetworkTest.java @@ -12,5 +12,6 @@ public class NetworkTest extends NetworkTestBase { @Test public void testRegions () throws Exception { modelTest("network/network_regions"); } @Test public void testGetNetworkKeys () throws Exception { modelTest("network/network_keys"); } + @Test public void testMinimalLaunch () throws Exception { modelTest("network/minimal_launch"); } } diff --git a/bubble-server/src/test/java/bubble/test/system/NetworkTestBase.java b/bubble-server/src/test/java/bubble/test/system/NetworkTestBase.java index 07d967cc..23573db2 100644 --- a/bubble-server/src/test/java/bubble/test/system/NetworkTestBase.java +++ b/bubble-server/src/test/java/bubble/test/system/NetworkTestBase.java @@ -4,10 +4,17 @@ */ package bubble.test.system; +import bubble.server.BubbleConfiguration; import bubble.test.ActivatedBubbleModelTestBase; +import org.cobbzilla.wizard.server.RestServer; public class NetworkTestBase extends ActivatedBubbleModelTestBase { @Override protected String getManifest() { return "manifest-network"; } + @Override public void beforeStart(RestServer server) { + mockNetwork(server); + super.beforeStart(server); + } + } diff --git a/bubble-server/src/test/resources/models/tests/network/minimal_launch.json b/bubble-server/src/test/resources/models/tests/network/minimal_launch.json new file mode 100644 index 00000000..2a4135db --- /dev/null +++ b/bubble-server/src/test/resources/models/tests/network/minimal_launch.json @@ -0,0 +1,69 @@ +[ + { + "comment": "create another account and login", + "include": "new_account", + "params": { + "email": "min_launch@example.com", + "verifyEmail": true + } + }, + + { + "comment": "list plans", + "request": { "uri": "plans" }, + "response": { + "store": "plans", + "check": [ + {"condition": "json.length > 0"} + ] + } + }, + + { + "comment": "add a payment method and preferred plan", + "before": "stripe_tokenize_card", + "request": { + "uri": "me/paymentMethods", + "method": "put", + "entity": { + "paymentMethodType": "credit", + "paymentInfo": "{{stripeToken}}", + "preferredPlan": "{{plans.[0].name}}" + } + } + }, + + { + "comment": "add account plan", + "request": { + "uri": "me/plans", + "method": "put", + "entity": {} + }, + "response": { "store": "plan" } + }, + + { + "comment": "start the network. sets up the first node, which does the rest", + "request": { + "uri": "me/networks/{{ plan.name }}/actions/start", + "method": "post" + }, + "response": { + "store": "<>" + } + }, + + { + "comment": "list networks, verify new network is starting", + "before": "sleep 10s", + "request": { "uri": "me/networks" }, + "response": { + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getName() === '{{plan.name}}'"}, + {"condition": "json[0].getState().name() === 'starting'"} + ] + } + } +] \ No newline at end of file