fix: permalink update when slug changed (#2382)

<!--  Thanks for sending a pull request!  Here are some tips for you:
1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>。
1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>.
2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request.
3. 请确保你已经添加并运行了适当的测试。
3. Ensure you have added or ran the appropriate tests for your PR.
-->

#### What type of PR is this?
/kind bug
/area core
/milestone 2.0
<!--
添加其中一个类别:
Add one of the following kinds:

/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind improvement

适当添加其中一个或多个类别(可选):
Optionally add one or more of the following kinds if applicable:

/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

#### What this PR does / why we need it:
修复文章 分类 标签的 slug 改变时,没有重新生成 permalink 的问题
#### Which issue(s) this PR fixes:

<!--
PR 合并时自动关闭 issue。
Automatically closes linked issue when PR is merged.

用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)`
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #

#### Special notes for your reviewer:
/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?

<!--
如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`。
否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新(Break Change),
Release Note 需要以 `action required` 开头。
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->

```release-note
None
```
pull/2392/head
guqing 2022-09-07 16:28:11 +08:00 committed by GitHub
parent 969fcde641
commit e25a3d2232
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 380 additions and 123 deletions

View File

@ -1,6 +1,30 @@
package run.halo.app.content.permalinks; package run.halo.app.content.permalinks;
import java.util.Objects;
import run.halo.app.extension.GroupVersionKind; import run.halo.app.extension.GroupVersionKind;
/**
* Slug can be modified, so it is not included in {@link #equals(Object)} and {@link #hashCode()}.
*
* @param gvk group version kind
* @param name extension name
* @param slug extension slug
*/
public record ExtensionLocator(GroupVersionKind gvk, String name, String slug) { public record ExtensionLocator(GroupVersionKind gvk, String name, String slug) {
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ExtensionLocator locator = (ExtensionLocator) o;
return gvk.equals(locator.gvk) && name.equals(locator.name);
}
@Override
public int hashCode() {
return Objects.hash(gvk, name);
}
} }

View File

@ -1,5 +1,7 @@
package run.halo.app.core.extension.reconciler; package run.halo.app.core.extension.reconciler;
import java.util.HashSet;
import java.util.Set;
import run.halo.app.content.permalinks.CategoryPermalinkPolicy; import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
import run.halo.app.core.extension.Category; import run.halo.app.core.extension.Category;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
@ -13,7 +15,7 @@ import run.halo.app.infra.utils.JsonUtils;
* @since 2.0.0 * @since 2.0.0
*/ */
public class CategoryReconciler implements Reconciler<Reconciler.Request> { public class CategoryReconciler implements Reconciler<Reconciler.Request> {
private static final String FINALIZER_NAME = "category-protection";
private final ExtensionClient client; private final ExtensionClient client;
private final CategoryPermalinkPolicy categoryPermalinkPolicy; private final CategoryPermalinkPolicy categoryPermalinkPolicy;
@ -26,25 +28,67 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
@Override @Override
public Result reconcile(Request request) { public Result reconcile(Request request) {
client.fetch(Category.class, request.name()) client.fetch(Category.class, request.name())
.ifPresent(category -> {
if (isDeleted(category)) {
cleanUpResourcesAndRemoveFinalizer(request.name());
return;
}
addFinalizerIfNecessary(category);
reconcileStatus(request.name());
});
return new Result(false, null);
}
private void addFinalizerIfNecessary(Category oldCategory) {
Set<String> finalizers = oldCategory.getMetadata().getFinalizers();
if (finalizers != null && finalizers.contains(FINALIZER_NAME)) {
return;
}
client.fetch(Category.class, oldCategory.getMetadata().getName())
.ifPresent(category -> {
Set<String> newFinalizers = category.getMetadata().getFinalizers();
if (newFinalizers == null) {
newFinalizers = new HashSet<>();
category.getMetadata().setFinalizers(newFinalizers);
}
newFinalizers.add(FINALIZER_NAME);
client.update(category);
});
}
private void cleanUpResources(Category category) {
// remove permalink from permalink indexer
categoryPermalinkPolicy.onPermalinkDelete(category);
}
private void cleanUpResourcesAndRemoveFinalizer(String categoryName) {
client.fetch(Category.class, categoryName).ifPresent(category -> {
cleanUpResources(category);
if (category.getMetadata().getFinalizers() != null) {
category.getMetadata().getFinalizers().remove(FINALIZER_NAME);
}
client.update(category);
});
}
private void reconcileStatus(String name) {
client.fetch(Category.class, name)
.ifPresent(category -> { .ifPresent(category -> {
Category oldCategory = JsonUtils.deepCopy(category); Category oldCategory = JsonUtils.deepCopy(category);
reconcilePermalink(category); categoryPermalinkPolicy.onPermalinkDelete(oldCategory);
category.getStatusOrDefault()
.setPermalink(categoryPermalinkPolicy.permalink(category));
categoryPermalinkPolicy.onPermalinkAdd(category);
if (!oldCategory.equals(category)) { if (!oldCategory.equals(category)) {
client.update(category); client.update(category);
} }
}); });
return new Result(false, null);
} }
private void reconcilePermalink(Category category) { private boolean isDeleted(Category category) {
category.getStatusOrDefault() return category.getMetadata().getDeletionTimestamp() != null;
.setPermalink(categoryPermalinkPolicy.permalink(category));
if (category.getMetadata().getDeletionTimestamp() != null) {
categoryPermalinkPolicy.onPermalinkDelete(category);
return;
}
categoryPermalinkPolicy.onPermalinkAdd(category);
} }
} }

View File

@ -33,6 +33,7 @@ import run.halo.app.infra.utils.JsonUtils;
* @since 2.0.0 * @since 2.0.0
*/ */
public class PostReconciler implements Reconciler<Reconciler.Request> { public class PostReconciler implements Reconciler<Reconciler.Request> {
private static final String FINALIZER_NAME = "post-protection";
private final ExtensionClient client; private final ExtensionClient client;
private final ContentService contentService; private final ContentService contentService;
private final PostPermalinkPolicy postPermalinkPolicy; private final PostPermalinkPolicy postPermalinkPolicy;
@ -48,39 +49,58 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
public Result reconcile(Request request) { public Result reconcile(Request request) {
client.fetch(Post.class, request.name()) client.fetch(Post.class, request.name())
.ifPresent(post -> { .ifPresent(post -> {
Post oldPost = JsonUtils.deepCopy(post); if (isDeleted(post)) {
cleanUpResourcesAndRemoveFinalizer(request.name());
return;
}
addFinalizerIfNecessary(post);
doReconcile(post); reconcileMetadata(request.name());
permalinkReconcile(post); reconcileStatus(request.name());
});
return new Result(false, null);
}
private void reconcileMetadata(String name) {
client.fetch(Post.class, name).ifPresent(post -> {
final Post oldPost = JsonUtils.deepCopy(post);
Post.PostSpec spec = post.getSpec();
// handle logic delete
Map<String, String> labels = getLabelsOrDefault(post);
if (Objects.equals(spec.getDeleted(), true)) {
labels.put(Post.DELETED_LABEL, Boolean.TRUE.toString());
} else {
labels.put(Post.DELETED_LABEL, Boolean.FALSE.toString());
}
// synchronize some fields to labels to query
labels.put(Post.PHASE_LABEL, post.getStatusOrDefault().getPhase());
labels.put(Post.VISIBLE_LABEL,
Objects.requireNonNullElse(spec.getVisible(), Post.VisibleEnum.PUBLIC).name());
labels.put(Post.OWNER_LABEL, spec.getOwner());
if (!oldPost.equals(post)) { if (!oldPost.equals(post)) {
client.update(post); client.update(post);
} }
}); });
return new Result(false, null);
} }
private void permalinkReconcile(Post post) { private void reconcileStatus(String name) {
client.fetch(Post.class, name).ifPresent(post -> {
final Post oldPost = JsonUtils.deepCopy(post);
postPermalinkPolicy.onPermalinkDelete(oldPost);
post.getStatusOrDefault() post.getStatusOrDefault()
.setPermalink(postPermalinkPolicy.permalink(post)); .setPermalink(postPermalinkPolicy.permalink(post));
if (isPublished(post)) {
if (Objects.equals(true, post.getSpec().getDeleted())
|| post.getMetadata().getDeletionTimestamp() != null
|| Objects.equals(false, post.getSpec().getPublished())) {
postPermalinkPolicy.onPermalinkDelete(post);
return;
}
postPermalinkPolicy.onPermalinkAdd(post); postPermalinkPolicy.onPermalinkAdd(post);
} }
private void doReconcile(Post post) {
String name = post.getMetadata().getName();
Post.PostSpec spec = post.getSpec();
Post.PostStatus status = post.getStatusOrDefault(); Post.PostStatus status = post.getStatusOrDefault();
if (status.getPhase() == null) { if (status.getPhase() == null) {
status.setPhase(Post.PostPhase.DRAFT.name()); status.setPhase(Post.PostPhase.DRAFT.name());
} }
Post.PostSpec spec = post.getSpec();
// handle excerpt // handle excerpt
Post.Excerpt excerpt = spec.getExcerpt(); Post.Excerpt excerpt = spec.getExcerpt();
if (excerpt == null) { if (excerpt == null) {
@ -88,7 +108,6 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
excerpt.setAutoGenerate(true); excerpt.setAutoGenerate(true);
spec.setExcerpt(excerpt); spec.setExcerpt(excerpt);
} }
if (excerpt.getAutoGenerate()) { if (excerpt.getAutoGenerate()) {
contentService.getContent(spec.getHeadSnapshot()) contentService.getContent(spec.getHeadSnapshot())
.subscribe(content -> { .subscribe(content -> {
@ -118,7 +137,8 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
// update in progress status // update in progress status
snapshots.stream() snapshots.stream()
.filter(snapshot -> snapshot.getMetadata().getName().equals(headSnapshot)) .filter(
snapshot -> snapshot.getMetadata().getName().equals(headSnapshot))
.findAny() .findAny()
.ifPresent(snapshot -> { .ifPresent(snapshot -> {
status.setInProgress(!isPublished(snapshot)); status.setInProgress(!isPublished(snapshot));
@ -139,19 +159,42 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
status.setPhase(Post.PostPhase.DRAFT.name()); status.setPhase(Post.PostPhase.DRAFT.name());
} }
// handle logic delete if (!oldPost.equals(post)) {
Map<String, String> labels = getLabelsOrDefault(post); client.update(post);
if (isDeleted(post)) {
labels.put(Post.DELETED_LABEL, Boolean.TRUE.toString());
// TODO do more about logic delete such as remove router
} else {
labels.put(Post.DELETED_LABEL, Boolean.FALSE.toString());
} }
// synchronize some fields to labels to query });
labels.put(Post.PHASE_LABEL, status.getPhase()); }
labels.put(Post.VISIBLE_LABEL,
Objects.requireNonNullElse(spec.getVisible(), Post.VisibleEnum.PUBLIC).name()); private void addFinalizerIfNecessary(Post oldPost) {
labels.put(Post.OWNER_LABEL, spec.getOwner()); Set<String> finalizers = oldPost.getMetadata().getFinalizers();
if (finalizers != null && finalizers.contains(FINALIZER_NAME)) {
return;
}
client.fetch(Post.class, oldPost.getMetadata().getName())
.ifPresent(post -> {
Set<String> newFinalizers = post.getMetadata().getFinalizers();
if (newFinalizers == null) {
newFinalizers = new HashSet<>();
post.getMetadata().setFinalizers(newFinalizers);
}
newFinalizers.add(FINALIZER_NAME);
client.update(post);
});
}
private void cleanUpResourcesAndRemoveFinalizer(String postName) {
client.fetch(Post.class, postName).ifPresent(post -> {
cleanUpResources(post);
if (post.getMetadata().getFinalizers() != null) {
post.getMetadata().getFinalizers().remove(FINALIZER_NAME);
}
client.update(post);
});
}
private void cleanUpResources(Post post) {
// remove permalink from permalink indexer
postPermalinkPolicy.onPermalinkDelete(post);
} }
private Map<String, String> getLabelsOrDefault(Post post) { private Map<String, String> getLabelsOrDefault(Post post) {
@ -175,6 +218,10 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
return snapshot.getSpec().getPublishTime() != null; return snapshot.getSpec().getPublishTime() != null;
} }
private boolean isPublished(Post post) {
return Objects.equals(true, post.getSpec().getPublished());
}
private boolean isDeleted(Post post) { private boolean isDeleted(Post post) {
return Objects.equals(true, post.getSpec().getDeleted()) return Objects.equals(true, post.getSpec().getDeleted())
|| post.getMetadata().getDeletionTimestamp() != null; || post.getMetadata().getDeletionTimestamp() != null;

View File

@ -1,5 +1,7 @@
package run.halo.app.core.extension.reconciler; package run.halo.app.core.extension.reconciler;
import java.util.HashSet;
import java.util.Set;
import run.halo.app.content.permalinks.TagPermalinkPolicy; import run.halo.app.content.permalinks.TagPermalinkPolicy;
import run.halo.app.core.extension.Tag; import run.halo.app.core.extension.Tag;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
@ -13,6 +15,7 @@ import run.halo.app.infra.utils.JsonUtils;
* @since 2.0.0 * @since 2.0.0
*/ */
public class TagReconciler implements Reconciler<Reconciler.Request> { public class TagReconciler implements Reconciler<Reconciler.Request> {
private static final String FINALIZER_NAME = "tag-protection";
private final ExtensionClient client; private final ExtensionClient client;
private final TagPermalinkPolicy tagPermalinkPolicy; private final TagPermalinkPolicy tagPermalinkPolicy;
@ -25,25 +28,66 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
public Result reconcile(Request request) { public Result reconcile(Request request) {
client.fetch(Tag.class, request.name()) client.fetch(Tag.class, request.name())
.ifPresent(tag -> { .ifPresent(tag -> {
Tag oldTag = JsonUtils.deepCopy(tag); if (isDeleted(tag)) {
cleanUpResourcesAndRemoveFinalizer(request.name());
this.reconcilePermalink(tag); return;
if (!tag.equals(oldTag)) {
client.update(tag);
} }
addFinalizerIfNecessary(tag);
this.reconcileStatus(request.name());
}); });
return new Result(false, null); return new Result(false, null);
} }
private void reconcilePermalink(Tag tag) { private void cleanUpResources(Tag tag) {
tag.getStatusOrDefault() // remove permalink from permalink indexer
.setPermalink(tagPermalinkPolicy.permalink(tag));
if (tag.getMetadata().getDeletionTimestamp() != null) {
tagPermalinkPolicy.onPermalinkDelete(tag); tagPermalinkPolicy.onPermalinkDelete(tag);
}
private void addFinalizerIfNecessary(Tag oldTag) {
Set<String> finalizers = oldTag.getMetadata().getFinalizers();
if (finalizers != null && finalizers.contains(FINALIZER_NAME)) {
return; return;
} }
client.fetch(Tag.class, oldTag.getMetadata().getName())
.ifPresent(tag -> {
Set<String> newFinalizers = tag.getMetadata().getFinalizers();
if (newFinalizers == null) {
newFinalizers = new HashSet<>();
tag.getMetadata().setFinalizers(newFinalizers);
}
newFinalizers.add(FINALIZER_NAME);
client.update(tag);
});
}
private void cleanUpResourcesAndRemoveFinalizer(String tagName) {
client.fetch(Tag.class, tagName).ifPresent(tag -> {
cleanUpResources(tag);
if (tag.getMetadata().getFinalizers() != null) {
tag.getMetadata().getFinalizers().remove(FINALIZER_NAME);
}
client.update(tag);
});
}
private void reconcileStatus(String tagName) {
client.fetch(Tag.class, tagName)
.ifPresent(tag -> {
Tag oldTag = JsonUtils.deepCopy(tag);
tagPermalinkPolicy.onPermalinkDelete(oldTag);
tag.getStatusOrDefault()
.setPermalink(tagPermalinkPolicy.permalink(tag));
tagPermalinkPolicy.onPermalinkAdd(tag); tagPermalinkPolicy.onPermalinkAdd(tag);
if (!oldTag.equals(tag)) {
client.update(tag);
}
});
}
private boolean isDeleted(Tag tag) {
return tag.getMetadata().getDeletionTimestamp() != null;
} }
} }

View File

@ -3,6 +3,7 @@ package run.halo.app.theme.router;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -52,6 +53,9 @@ public class PermalinkIndexer {
List<String> permalinks = permalinkLookup.get(locator.gvk()); List<String> permalinks = permalinkLookup.get(locator.gvk());
if (permalinks != null) { if (permalinks != null) {
permalinks.remove(permalink); permalinks.remove(permalink);
if (permalinks.isEmpty()) {
permalinkLookup.remove(locator.gvk());
}
} }
permalinkLocatorMap.remove(permalink); permalinkLocatorMap.remove(permalink);
} finally { } finally {
@ -83,7 +87,7 @@ public class PermalinkIndexer {
public List<String> getPermalinks(GroupVersionKind gvk) { public List<String> getPermalinks(GroupVersionKind gvk) {
readWriteLock.readLock().lock(); readWriteLock.readLock().lock();
try { try {
return permalinkLookup.get(gvk); return Objects.requireNonNullElse(permalinkLookup.get(gvk), List.of());
} finally { } finally {
readWriteLock.readLock().unlock(); readWriteLock.readLock().unlock();
} }
@ -196,7 +200,7 @@ public class PermalinkIndexer {
@EventListener(PermalinkIndexDeleteCommand.class) @EventListener(PermalinkIndexDeleteCommand.class)
public void onPermalinkDelete(PermalinkIndexDeleteCommand deleteCommand) { public void onPermalinkDelete(PermalinkIndexDeleteCommand deleteCommand) {
register(deleteCommand.getLocator(), deleteCommand.getPermalink()); remove(deleteCommand.getLocator(), deleteCommand.getPermalink());
} }
@EventListener(PermalinkIndexUpdateCommand.class) @EventListener(PermalinkIndexUpdateCommand.class)

View File

@ -10,10 +10,10 @@ import static org.mockito.Mockito.when;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
@ -43,17 +43,14 @@ class PostReconcilerTest {
@Mock @Mock
private PostPermalinkPolicy postPermalinkPolicy; private PostPermalinkPolicy postPermalinkPolicy;
@InjectMocks
private PostReconciler postReconciler; private PostReconciler postReconciler;
@BeforeEach
void setUp() {
postReconciler = new PostReconciler(client, contentService, postPermalinkPolicy);
}
@Test @Test
void reconcile() { void reconcile() {
String name = "post-A"; String name = "post-A";
Post post = TestPost.postV1(); Post post = TestPost.postV1();
post.getSpec().setPublished(false);
post.getSpec().setHeadSnapshot("post-A-head-snapshot"); post.getSpec().setHeadSnapshot("post-A-head-snapshot");
when(client.fetch(eq(Post.class), eq(name))) when(client.fetch(eq(Post.class), eq(name)))
.thenReturn(Optional.of(post)); .thenReturn(Optional.of(post));
@ -72,7 +69,12 @@ class PostReconcilerTest {
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class); ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
postReconciler.reconcile(new Reconciler.Request(name)); postReconciler.reconcile(new Reconciler.Request(name));
verify(client, times(1)).update(captor.capture()); verify(client, times(3)).update(captor.capture());
verify(postPermalinkPolicy, times(1)).permalink(any());
verify(postPermalinkPolicy, times(0)).onPermalinkAdd(any());
verify(postPermalinkPolicy, times(1)).onPermalinkDelete(any());
verify(postPermalinkPolicy, times(0)).onPermalinkUpdate(any());
Post value = captor.getValue(); Post value = captor.getValue();
assertThat(value.getStatus().getExcerpt()).isEqualTo("hello world"); assertThat(value.getStatus().getExcerpt()).isEqualTo("hello world");

View File

@ -0,0 +1,92 @@
package run.halo.app.core.extension.reconciler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.Instant;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import run.halo.app.content.permalinks.TagPermalinkPolicy;
import run.halo.app.core.extension.Tag;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Metadata;
/**
* Tests for {@link TagReconciler}.
*
* @author guqing
* @since 2.0.0
*/
@ExtendWith(MockitoExtension.class)
class TagReconcilerTest {
@Mock
private ExtensionClient client;
@Mock
private TagPermalinkPolicy tagPermalinkPolicy;
@InjectMocks
private TagReconciler tagReconciler;
@Test
void reconcile() {
Tag tag = tag();
when(client.fetch(eq(Tag.class), eq("fake-tag")))
.thenReturn(Optional.of(tag));
when(tagPermalinkPolicy.permalink(any()))
.thenAnswer(arg -> "/tags/" + tag.getSpec().getSlug());
ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class);
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
verify(client, times(2)).update(captor.capture());
verify(tagPermalinkPolicy, times(1)).onPermalinkAdd(any());
verify(tagPermalinkPolicy, times(1)).onPermalinkDelete(any());
Tag capture = captor.getValue();
assertThat(capture.getStatus().getPermalink()).isEqualTo("/tags/fake-slug");
// change slug
tag.getSpec().setSlug("new-slug");
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
verify(client, times(3)).update(captor.capture());
verify(tagPermalinkPolicy, times(2)).onPermalinkAdd(any());
verify(tagPermalinkPolicy, times(2)).onPermalinkDelete(any());
assertThat(capture.getStatus().getPermalink()).isEqualTo("/tags/new-slug");
}
@Test
void reconcileDelete() {
Tag tag = tag();
tag.getMetadata().setDeletionTimestamp(Instant.now());
when(client.fetch(eq(Tag.class), eq("fake-tag")))
.thenReturn(Optional.of(tag));
ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class);
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
verify(client, times(1)).update(captor.capture());
verify(tagPermalinkPolicy, times(0)).onPermalinkAdd(any());
verify(tagPermalinkPolicy, times(1)).onPermalinkDelete(any());
verify(tagPermalinkPolicy, times(0)).permalink(any());
}
Tag tag() {
Tag tag = new Tag();
tag.setMetadata(new Metadata());
tag.getMetadata().setName("fake-tag");
tag.setSpec(new Tag.TagSpec());
tag.getSpec().setSlug("fake-slug");
tag.setStatus(new Tag.TagStatus());
return tag;
}
}