mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
5 days ago 0138b5924ff14ef709398bf878dbb9bf2a0b7c33
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.quicksetup.installer;
 
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.opends.messages.QuickSetupMessages.*;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
 
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
 
import org.forgerock.i18n.LocalizableMessage;
import org.opends.quicksetup.TempLogFile;
import org.opends.quicksetup.UserData;
import org.opends.quicksetup.event.ProgressUpdateEvent;
import org.opends.quicksetup.event.ProgressUpdateListener;
import org.opends.quicksetup.util.PlainTextProgressMessageFormatter;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
 
/**
 * Tests what a failed installation tells its listeners about the log file.
 * <p>
 * This report is the one place the diagnosis of a failed setup lives, and it used to promise
 * the file without looking at it - printing a {@code NoSuchFileException} stack when the file
 * was gone (issue #1030). The roads below are the whole of that decision.
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "quicksetup" }, sequential = true)
public class InstallerTest extends DirectoryServerTestCase
{
  private static final String PREFIX = "opendj-setup-";
  /** Marks a line the report sends out as a warning, see {@link MarkedArmsFormatter}. */
  private static final String WARNING = "[W]";
  /** Marks a line the report sends out as progress. */
  private static final String PROGRESS = "[P]";
 
  private File tempDir;
  private final List<TempLogFile> created = new ArrayList<>();
 
  @BeforeClass
  public void setUp() throws IOException
  {
    tempDir = TestCaseUtils.createTemporaryDirectory("installerTest");
  }
 
  @AfterClass
  public void tearDown() throws IOException
  {
    for (TempLogFile logFile : created)
    {
      logFile.deleteLogFileAfterSuccess();
    }
    TestCaseUtils.deleteDirectory(tempDir);
  }
 
  /** A log that is there is named, as before, and named as progress rather than as a warning. */
  @Test
  public void testAReadableLogIsHandedOver() throws Exception
  {
    final TempLogFile logFile = newLogFile();
 
    final String report = reportOf(logFile);
 
    assertContains(report, PROGRESS, INFO_GENERAL_PROVIDE_LOG_IN_ERROR.get(logFile.getPath()));
    assertDoesNotContain(report, INFO_GENERAL_LOG_IN_ERROR_MISSING.get(logFile.getPath()));
  }
 
  /**
   * The contents of the log reach the report: attaching the file is up to whoever reads it,
   * so the report carries the log itself.
   * <p>
   * What the file holds is not the point here and is mocked away - with
   * {@code OPENDJ_LOG_TO_STDOUT} set, a real log is empty and would pin nothing.
   */
  @Test
  public void testTheContentsOfTheLogReachTheReport() throws Exception
  {
    final String contents = "QuickSetup application launched\nthe last line before the failure\n";
    final TempLogFile logFile = mock(TempLogFile.class);
    when(logFile.isEnabled()).thenReturn(true);
    when(logFile.isReadable()).thenReturn(true);
    when(logFile.getPath()).thenReturn(new File(tempDir, "readable.log").getAbsolutePath());
    when(logFile.readContents()).thenReturn(contents);
 
    final String report = reportOf(logFile);
 
    assertTrue(report.contains(contents), report);
  }
 
  /** A log something else removed is reported as gone, instead of being asked for. */
  @Test
  public void testAMissingLogIsReportedAsMissing() throws Exception
  {
    final TempLogFile logFile = newLogFile();
    final String path = logFile.getPath();
    logFile.deleteLogFileAfterSuccess();
    assertFalse(logFile.isReadable());
 
    final String report = reportOf(logFile);
 
    assertContains(report, WARNING, INFO_GENERAL_LOG_IN_ERROR_MISSING.get(path));
    // The line that asks for the file must not go out when there is no file to provide.
    assertDoesNotContain(report, INFO_GENERAL_PROVIDE_LOG_IN_ERROR.get(path));
  }
 
  /** A log which is there but cannot be read costs the report its contents, not its diagnosis. */
  @Test
  public void testAnUnreadableLogIsReportedAsUnreadable() throws Exception
  {
    final String path = new File(tempDir, "unreadable.log").getAbsolutePath();
    final IOException failure = new IOException("Input/output error");
    final TempLogFile logFile = mock(TempLogFile.class);
    when(logFile.isEnabled()).thenReturn(true);
    when(logFile.isReadable()).thenReturn(true);
    when(logFile.getPath()).thenReturn(path);
    when(logFile.readContents()).thenThrow(failure);
 
    final String report = reportOf(logFile);
 
    assertContains(report, WARNING, INFO_GENERAL_LOG_IN_ERROR_UNREADABLE.get(path, failure));
    assertDoesNotContain(report, INFO_GENERAL_LOG_IN_ERROR_MISSING.get(path));
  }
 
  /** Nothing is said about a log that was never created. */
  @Test
  public void testNoLogMeansNoReport() throws Exception
  {
    final TempLogFile logFile = mock(TempLogFile.class);
    when(logFile.isEnabled()).thenReturn(false);
 
    assertTrue(reportOf(logFile).isEmpty(), "a launcher without a log has nothing to report");
  }
 
  /**
   * A cancelled install takes its log with it - and asks for it only when it begins.
   * <p>
   * Nothing names the log on that road - the report belongs to the failure road - and the
   * cancel has just taken the installation back, so a log kept there is a report nobody is
   * ever pointed at (issue #1030). The installer is handed the log the way the wizard and the
   * command line hand it over, as a supplier: nothing asks for it before {@code run()}.
   */
  @Test
  public void testACancelledInstallTakesItsLogWithIt() throws Exception
  {
    final TempLogFile logFile = newLogFile();
    final File instance = new File(tempDir, "cancelled");
    // A locks directory the lock file can be taken in: without it the cancel road reads the
    // server as running and goes off to stop it.
    assertTrue(new File(instance, "locks").mkdirs());
 
    final TestInstaller installer = new TestInstaller(instance);
    installer.setProgressMessageFormatter(new MarkedArmsFormatter());
    final int[] asked = { 0 };
    installer.setTempLogFile(() -> {
      asked[0]++;
      return logFile;
    });
    installer.setUserData(new UserData());
    installer.cancel();
    assertEquals(asked[0], 0, "nothing is logged before the install begins");
 
    installer.run();
 
    assertEquals(asked[0], 1, "the install asks for its log once, as it begins");
    assertEquals(installer.getCurrentProgressStep(), InstallProgressStep.FINISHED_CANCELED);
    assertFalse(logFile.getLogFile().exists(), logFile.getPath());
  }
 
  /**
   * An application nobody handed a log to has none, and asking for it is not a failure: the
   * command line uninstaller is built that way, its launcher having created the log already.
   */
  @Test
  public void testAnApplicationGivenNoLogHasNone() throws Exception
  {
    assertNull(new TestInstaller(null).openTempLogFile());
  }
 
  private TempLogFile newLogFile()
  {
    final TempLogFile logFile = TempLogFile.newTempLogFile(PREFIX, new File(tempDir, "logs"));
    created.add(logFile);
    return logFile;
  }
 
  /** What the listeners of a failed installation are told about the log file. */
  private static String reportOf(final TempLogFile logFile)
  {
    final TestInstaller installer = new TestInstaller(null);
    installer.setProgressMessageFormatter(new MarkedArmsFormatter());
    final StringBuilder report = new StringBuilder();
    installer.addProgressUpdateListener(new ProgressUpdateListener()
    {
      @Override
      public void progressUpdate(final ProgressUpdateEvent ev)
      {
        if (ev.getNewLogs() != null)
        {
          report.append(ev.getNewLogs());
        }
      }
    });
    installer.setTempLogFile(() -> logFile);
    // The report is made by an install which has begun, and so has asked for its log.
    installer.openTempLogFile();
    installer.notifyListenersOfExistingLogFile();
    return report.toString();
  }
 
  private static void assertContains(final String report, final String arm, final LocalizableMessage expected)
  {
    assertTrue(report.contains(arm + expected), "expected <" + arm + expected + "> in <" + report + ">");
  }
 
  private static void assertDoesNotContain(final String report, final LocalizableMessage unexpected)
  {
    assertFalse(report.contains(unexpected.toString()), "unexpected <" + unexpected + "> in <" + report + ">");
  }
 
  /** An installer which can be told where it lives, and whose log can be asked for. */
  private static final class TestInstaller extends Installer
  {
    /** The installation, or {@code null} for the one the class path names. */
    private final File instance;
 
    TestInstaller(final File instance)
    {
      this.instance = instance;
    }
 
    /**
     * An installation under the temporary directory rather than the one the class path names:
     * the installer takes both paths from there, and in a test run there is none.
     */
    @Override
    public String getInstallationPath()
    {
      return instance != null ? instance.getAbsolutePath() : super.getInstallationPath();
    }
 
    @Override
    public String getInstancePath()
    {
      return instance != null ? instance.getAbsolutePath() : super.getInstancePath();
    }
 
    @Override
    protected TempLogFile openTempLogFile()
    {
      return super.openTempLogFile();
    }
  }
 
  /**
   * A formatter that marks the arm each line goes out on.
   * <p>
   * The plain text formatter returns warnings and progress messages unchanged - only the
   * wizard's HTML formatter tells them apart - so without a mark a report which says "the log
   * is gone" as an ordinary progress line reads exactly like one which warns about it.
   */
  private static final class MarkedArmsFormatter extends PlainTextProgressMessageFormatter
  {
    @Override
    public LocalizableMessage getFormattedWarning(final LocalizableMessage text, final boolean applyMargin)
    {
      return LocalizableMessage.raw(WARNING + super.getFormattedWarning(text, applyMargin));
    }
 
    @Override
    public LocalizableMessage getFormattedProgress(final LocalizableMessage text)
    {
      return LocalizableMessage.raw(PROGRESS + super.getFormattedProgress(text));
    }
  }
}