aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java
blob: 4056459c532a72eb366434d0bee25f02158dc492 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.maintenance;

import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.test.ManualClock;
import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.flags.InMemoryFlagSource;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
import com.yahoo.vespa.hosted.controller.notification.Notification;
import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * @author ogronnesby
 */
public class CloudTrialExpirerTest {

    private static final boolean OVERWRITE_TEST_FILES = false;

    private final ControllerTester tester = new ControllerTester(SystemName.PublicCd);
    private final DeploymentTester deploymentTester = new DeploymentTester(tester);
    private final CloudTrialExpirer expirer = new CloudTrialExpirer(tester.controller(), Duration.ofMinutes(5));

    @Test
    void expire_inactive_tenant() {
        registerTenant("trial-tenant", "trial", Duration.ofDays(14).plusMillis(1));
        assertEquals(0.0, expirer.maintain());
        assertPlan("trial-tenant", "none");
    }

    @Test
    void tombstone_inactive_none() {
        registerTenant("none-tenant", "none", Duration.ofDays(91).plusMillis(1));
        assertEquals(0.0, expirer.maintain());
        assertEquals(Tenant.Type.deleted, tester.controller().tenants().get(TenantName.from("none-tenant"), true).get().type());
    }

    @Test
    void keep_inactive_nontrial_tenants() {
        registerTenant("not-a-trial-tenant", "pay-as-you-go", Duration.ofDays(30));
        assertEquals(0.0, expirer.maintain());
        assertPlan("not-a-trial-tenant", "pay-as-you-go");
    }

    @Test
    void keep_active_trial_tenants() {
        registerTenant("active-trial-tenant", "trial", Duration.ofHours(14).minusMillis(1));
        assertEquals(0.0, expirer.maintain());
        assertPlan("active-trial-tenant", "trial");
    }

    @Test
    void keep_inactive_exempt_tenants() {
        registerTenant("exempt-trial-tenant", "trial", Duration.ofDays(40));
        ((InMemoryFlagSource) tester.controller().flagSource()).withListFlag(PermanentFlags.EXTENDED_TRIAL_TENANTS.id(), List.of("exempt-trial-tenant"), String.class);
        assertEquals(0.0, expirer.maintain());
        assertPlan("exempt-trial-tenant", "trial");
    }

    @Test
    void keep_inactive_trial_tenants_with_deployments() {
        registerTenant("with-deployments", "trial", Duration.ofDays(30));
        registerDeployment("with-deployments", "my-app", "default");
        assertEquals(0.0, expirer.maintain());
        assertPlan("with-deployments", "trial");
    }

    @Test
    void delete_tenants_with_applications_with_no_deployments() {
        registerTenant("with-apps", "trial", Duration.ofDays(184));
        tester.createApplication("with-apps", "app1", "instance1");
        assertEquals(0.0, expirer.maintain());
        assertPlan("with-apps", "none");
        assertEquals(0.0, expirer.maintain());
        assertTrue(tester.controller().tenants().get("with-apps").isEmpty());
    }

    @Test
    void keep_tenants_without_applications_that_are_idle() {
        registerTenant("active", "none", Duration.ofDays(182));
        assertEquals(0.0, expirer.maintain());
        assertPlan("active", "none");
    }

    @Test
    void queues_trial_notification_based_on_account_age() throws IOException {
        var clock = (ManualClock)tester.controller().clock();
        var mailer = (MockMailer) tester.serviceRegistry().mailer();
        var tenant = TenantName.from("trial-tenant");
        ((InMemoryFlagSource) tester.controller().flagSource())
                .withBooleanFlag(Flags.CLOUD_TRIAL_NOTIFICATIONS.id(), true);
        registerTenant(tenant.value(), "trial", Duration.ZERO);
        assertEquals(0.0, expirer.maintain());
        var expected = "Welcome to Vespa Cloud trial! [Manage plan](https://console.tld/tenant/trial-tenant/account/billing)";
        assertEquals(expected, lastAccountLevelNotificationTitle(tenant));
        assertLastEmailEquals(mailer, "welcome.html");

        expected = "You're halfway through the **14 day** trial period. [Manage plan](https://console.tld/tenant/trial-tenant/account/billing)";
        clock.advance(Duration.ofDays(7));
        assertEquals(0.0, expirer.maintain());
        assertEquals(expected, lastAccountLevelNotificationTitle(tenant));
        assertLastEmailEquals(mailer, "trial-reminder.html");

        expected = "Your Vespa Cloud trial expires in **2** days. [Manage plan](https://console.tld/tenant/trial-tenant/account/billing)";
        clock.advance(Duration.ofDays(5));
        assertEquals(0.0, expirer.maintain());
        assertEquals(expected, lastAccountLevelNotificationTitle(tenant));
        assertLastEmailEquals(mailer, "trial-expiring-soon.html");

        expected = "Your Vespa Cloud trial expires **tomorrow**. [Manage plan](https://console.tld/tenant/trial-tenant/account/billing)";
        clock.advance(Duration.ofDays(1));
        assertEquals(0.0, expirer.maintain());
        assertEquals(expected, lastAccountLevelNotificationTitle(tenant));
        assertLastEmailEquals(mailer, "trial-expiring-immediately.html");

        expected = "Your Vespa Cloud trial has expired. [Upgrade plan](https://console.tld/tenant/trial-tenant/account/billing)";
        clock.advance(Duration.ofDays(2));
        assertEquals(0.0, expirer.maintain());
        assertEquals(expected, lastAccountLevelNotificationTitle(tenant));
        assertLastEmailEquals(mailer, "trial-expired.html");
    }

    private void assertLastEmailEquals(MockMailer mailer, String expectedContentFile) throws IOException {
        var mails = mailer.inbox("dev-trial-tenant");
        assertFalse(mails.isEmpty());
        var content = mails.get(mails.size() - 1).htmlMessage().orElseThrow();
        var path = Paths.get("src/test/resources/mail/" + expectedContentFile);
        if (OVERWRITE_TEST_FILES) {
            Files.write(path, content.getBytes(),
                        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
        } else {
            var expectedContent = Files.readString(path);
            assertEquals(expectedContent, content);
        }
    }

    private void registerTenant(String tenantName, String plan, Duration timeSinceLastLogin) {
        var name = TenantName.from(tenantName);
        tester.createTenant(tenantName, Tenant.Type.cloud);
        tester.serviceRegistry().billingController().setPlan(name, PlanId.from(plan), false, false);
        tester.controller().tenants().updateLastLogin(name, List.of(LastLoginInfo.UserLevel.user), tester.controller().clock().instant().minus(timeSinceLastLogin));
    }

    private void registerDeployment(String tenantName, String appName, String instanceName) {
        var app = tester.createApplication(tenantName, appName, instanceName);
        var ctx = deploymentTester.newDeploymentContext(tenantName, appName, instanceName);
        var pkg = new ApplicationPackageBuilder()
                .instances("default")
                .region("aws-us-east-1c")
                .trustDefaultCertificate()
                .build();
        ctx.submit(pkg).deploy();
    }

    private void assertPlan(String tenant, String planId) {
        assertEquals(planId, tester.serviceRegistry().billingController().getPlan(TenantName.from(tenant)).value());
    }

    private String lastAccountLevelNotificationTitle(TenantName tenant) {
        return tester.controller().notificationsDb()
                .listNotifications(NotificationSource.from(tenant), false).stream()
                .filter(n -> n.type() == Notification.Type.account).map(Notification::title)
                .findFirst().orElseThrow();
    }

}